Commit f08be33b authored by Fatih Acet's avatar Fatih Acet

Merge branch '32464-detail-view-of-sentry-error' into 'master'

Sentry error details view

See merge request gitlab-org/gitlab!18878
parents 333d4236 791794a0
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import dateFormat from 'dateformat';
import { __, sprintf } from '~/locale';
import { GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import Stacktrace from './stacktrace.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { trackClickErrorLinkToSentryOptions } from '../utils';
export default {
components: {
GlButton,
GlLink,
GlLoadingIcon,
TooltipOnTruncate,
Icon,
Stacktrace,
},
directives: {
TrackEvent: TrackEventDirective,
},
mixins: [timeagoMixin],
props: {
issueDetailsPath: {
type: String,
required: true,
},
issueStackTracePath: {
type: String,
required: true,
},
},
computed: {
...mapState('details', ['error', 'loading', 'loadingStacktrace', 'stacktraceData']),
...mapGetters('details', ['stacktrace']),
reported() {
return sprintf(
__('Reported %{timeAgo} by %{reportedBy}'),
{
reportedBy: `<strong>${this.error.culprit}</strong>`,
timeAgo: this.timeFormated(this.stacktraceData.date_received),
},
false,
);
},
firstReleaseLink() {
return `${this.error.external_base_url}/releases/${this.error.first_release_short_version}`;
},
lastReleaseLink() {
return `${this.error.external_base_url}releases/${this.error.last_release_short_version}`;
},
showDetails() {
return Boolean(!this.loading && this.error && this.error.id);
},
showStacktrace() {
return Boolean(!this.loadingStacktrace && this.stacktrace && this.stacktrace.length);
},
},
mounted() {
this.startPollingDetails(this.issueDetailsPath);
this.startPollingStacktrace(this.issueStackTracePath);
},
methods: {
...mapActions('details', ['startPollingDetails', 'startPollingStacktrace']),
trackClickErrorLinkToSentryOptions,
formatDate(date) {
return `${this.timeFormated(date)} (${dateFormat(date, 'UTC:yyyy-mm-dd h:MM:ssTT Z')})`;
},
},
};
</script>
<template>
<div>
<div v-if="loading" class="py-3">
<gl-loading-icon :size="3" />
</div>
<div v-else-if="showDetails" class="error-details">
<div class="top-area align-items-center justify-content-between py-3">
<span v-if="!loadingStacktrace && stacktrace" v-html="reported"></span>
<!-- <gl-button class="my-3 ml-auto" variant="success">
{{ __('Create Issue') }}
</gl-button>-->
</div>
<div>
<tooltip-on-truncate :title="error.title" truncate-target="child" placement="top">
<h2 class="text-truncate">{{ error.title }}</h2>
</tooltip-on-truncate>
<h3>{{ __('Error details') }}</h3>
<ul>
<li>
<span class="bold">{{ __('Sentry event') }}:</span>
<gl-link
v-track-event="trackClickErrorLinkToSentryOptions(error.external_url)"
:href="error.external_url"
target="_blank"
>
<span class="text-truncate">{{ error.external_url }}</span>
<icon name="external-link" class="ml-1 flex-shrink-0" />
</gl-link>
</li>
<li v-if="error.first_release_short_version">
<span class="bold">{{ __('First seen') }}:</span>
{{ formatDate(error.first_seen) }}
<gl-link :href="firstReleaseLink" target="_blank">
<span>{{ __('Release') }}: {{ error.first_release_short_version }}</span>
</gl-link>
</li>
<li v-if="error.last_release_short_version">
<span class="bold">{{ __('Last seen') }}:</span>
{{ formatDate(error.last_seen) }}
<gl-link :href="lastReleaseLink" target="_blank">
<span>{{ __('Release') }}: {{ error.last_release_short_version }}</span>
</gl-link>
</li>
<li>
<span class="bold">{{ __('Events') }}:</span>
<span>{{ error.count }}</span>
</li>
<li>
<span class="bold">{{ __('Users') }}:</span>
<span>{{ error.user_count }}</span>
</li>
</ul>
<div v-if="loadingStacktrace" class="py-3">
<gl-loading-icon :size="3" />
</div>
<template v-if="showStacktrace">
<h3 class="my-4">{{ __('Stack trace') }}</h3>
<stacktrace :entries="stacktrace" />
</template>
</div>
</div>
</div>
</template>
...@@ -8,11 +8,12 @@ import { ...@@ -8,11 +8,12 @@ import {
GlTable, GlTable,
GlSearchBoxByType, GlSearchBoxByType,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { __ } from '~/locale'; import { __ } from '~/locale';
import TrackEventDirective from '~/vue_shared/directives/track_event'; import TrackEventDirective from '~/vue_shared/directives/track_event';
import { trackViewInSentryOptions, trackClickErrorLinkToSentryOptions } from '../utils'; import { trackViewInSentryOptions } from '../utils';
export default { export default {
fields: [ fields: [
...@@ -62,8 +63,8 @@ export default { ...@@ -62,8 +63,8 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(['errors', 'externalUrl', 'loading']), ...mapState('list', ['errors', 'externalUrl', 'loading']),
...mapGetters(['filterErrorsByTitle']), ...mapGetters('list', ['filterErrorsByTitle']),
filteredErrors() { filteredErrors() {
return this.errorSearchQuery ? this.filterErrorsByTitle(this.errorSearchQuery) : this.errors; return this.errorSearchQuery ? this.filterErrorsByTitle(this.errorSearchQuery) : this.errors;
}, },
...@@ -74,9 +75,11 @@ export default { ...@@ -74,9 +75,11 @@ export default {
} }
}, },
methods: { methods: {
...mapActions(['startPolling', 'restartPolling']), ...mapActions('list', ['startPolling', 'restartPolling']),
trackViewInSentryOptions, trackViewInSentryOptions,
trackClickErrorLinkToSentryOptions, viewDetails(errorId) {
visitUrl(`error_tracking/${errorId}/details`);
},
}, },
}; };
</script> </script>
...@@ -125,13 +128,11 @@ export default { ...@@ -125,13 +128,11 @@ export default {
<template slot="error" slot-scope="errors"> <template slot="error" slot-scope="errors">
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<gl-link <gl-link
v-track-event="trackClickErrorLinkToSentryOptions(errors.item.externalUrl)"
:href="errors.item.externalUrl"
class="d-flex text-dark" class="d-flex text-dark"
target="_blank" target="_blank"
@click="viewDetails(errors.item.id)"
> >
<strong class="text-truncate">{{ errors.item.title.trim() }}</strong> <strong class="text-truncate">{{ errors.item.title.trim() }}</strong>
<icon name="external-link" class="ml-1 flex-shrink-0" />
</gl-link> </gl-link>
<span class="text-secondary text-truncate"> <span class="text-secondary text-truncate">
{{ errors.item.culprit }} {{ errors.item.culprit }}
......
<script>
import StackTraceEntry from './stacktrace_entry.vue';
export default {
components: {
StackTraceEntry,
},
props: {
entries: {
type: Array,
required: true,
},
},
methods: {
isFirstEntry(index) {
return index === 0;
},
},
};
</script>
<template>
<div class="stacktrace">
<stack-trace-entry
v-for="(entry, index) in entries"
:key="`stacktrace-entry-${index}`"
:lines="entry.context"
:file-path="entry.filename"
:error-line="entry.lineNo"
:expanded="isFirstEntry(index)"
/>
</div>
</template>
<script>
import { GlTooltip } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
ClipboardButton,
FileIcon,
Icon,
},
directives: {
GlTooltip,
},
props: {
lines: {
type: Array,
required: true,
},
filePath: {
type: String,
required: true,
},
errorLine: {
type: Number,
required: true,
},
expanded: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
isExpanded: this.expanded,
};
},
computed: {
linesLength() {
return this.lines.length;
},
collapseIcon() {
return this.isExpanded ? 'chevron-down' : 'chevron-right';
},
},
methods: {
isHighlighted(lineNum) {
return lineNum === this.errorLine;
},
toggle() {
this.isExpanded = !this.isExpanded;
},
lineNum(line) {
return line[0];
},
lineCode(line) {
return line[1];
},
},
userColorScheme: window.gon.user_color_scheme,
};
</script>
<template>
<div class="file-holder">
<div ref="header" class="file-title file-title-flex-parent">
<div class="file-header-content ">
<div class="d-inline-block cursor-pointer" @click="toggle()">
<icon :name="collapseIcon" :size="16" aria-hidden="true" class="append-right-5" />
</div>
<div class="d-inline-block append-right-4">
<file-icon
:file-name="filePath"
:size="18"
aria-hidden="true"
css-classes="append-right-5"
/>
<strong v-gl-tooltip :title="filePath" class="file-title-name" data-container="body">
{{ filePath }}
</strong>
</div>
<clipboard-button
:title="__('Copy file path')"
:text="filePath"
css-class="btn-default btn-transparent btn-clipboard"
/>
</div>
</div>
<table v-if="isExpanded" :class="$options.userColorScheme" class="code js-syntax-highlight">
<tbody>
<template v-for="(line, index) in lines">
<tr :key="`stacktrace-line-${index}`" class="line_holder">
<td class="diff-line-num" :class="{ old: isHighlighted(lineNum(line)) }">
{{ lineNum(line) }}
</td>
<td
class="line_content"
:class="{ old: isHighlighted(lineNum(line)) }"
v-html="lineCode(line)"
></td>
</tr>
</template>
</tbody>
</table>
</div>
</template>
import Vue from 'vue';
import store from './store';
import ErrorDetails from './components/error_details.vue';
export default () => {
// eslint-disable-next-line no-new
new Vue({
el: '#js-error_details',
components: {
ErrorDetails,
},
store,
render(createElement) {
const domEl = document.querySelector(this.$options.el);
const { issueDetailsPath, issueStackTracePath } = domEl.dataset;
return createElement('error-details', {
props: {
issueDetailsPath,
issueStackTracePath,
},
});
},
});
};
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
export default { export default {
getErrorList({ endpoint }) { getSentryData({ endpoint }) {
return axios.get(endpoint); return axios.get(endpoint);
}, },
}; };
import service from '../../services';
import * as types from './mutation_types';
import createFlash from '~/flash';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
let stackTracePoll;
let detailPoll;
const stopPolling = poll => {
if (poll) poll.stop();
};
export function startPollingDetails({ commit }, endpoint) {
detailPoll = new Poll({
resource: service,
method: 'getSentryData',
data: { endpoint },
successCallback: ({ data }) => {
if (!data) {
detailPoll.restart();
return;
}
commit(types.SET_ERROR, data.error);
commit(types.SET_LOADING, false);
stopPolling(detailPoll);
},
errorCallback: () => {
commit(types.SET_LOADING, false);
createFlash(__('Failed to load error details from Sentry.'));
},
});
detailPoll.makeRequest();
}
export function startPollingStacktrace({ commit }, endpoint) {
stackTracePoll = new Poll({
resource: service,
method: 'getSentryData',
data: { endpoint },
successCallback: ({ data }) => {
if (!data) {
stackTracePoll.restart();
return;
}
commit(types.SET_STACKTRACE_DATA, data.error);
commit(types.SET_LOADING_STACKTRACE, false);
stopPolling(stackTracePoll);
},
errorCallback: () => {
commit(types.SET_LOADING_STACKTRACE, false);
createFlash(__('Failed to load stacktrace.'));
},
});
stackTracePoll.makeRequest();
}
export default () => {};
export const stacktrace = state => state.stacktraceData.stack_trace_entries.reverse();
export default () => {};
export const SET_ERROR = 'SET_ERRORS';
export const SET_LOADING = 'SET_LOADING';
export const SET_LOADING_STACKTRACE = 'SET_LOADING_STACKTRACE';
export const SET_STACKTRACE_DATA = 'SET_STACKTRACE_DATA';
import * as types from './mutation_types';
export default {
[types.SET_ERROR](state, data) {
state.error = data;
},
[types.SET_LOADING](state, loading) {
state.loading = loading;
},
[types.SET_LOADING_STACKTRACE](state, data) {
state.loadingStacktrace = data;
},
[types.SET_STACKTRACE_DATA](state, data) {
state.stacktraceData = data;
},
};
export default () => ({
error: {},
stacktraceData: {},
loading: true,
loadingStacktrace: true,
});
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters'; import * as listActions from './list/actions';
import mutations from './mutations'; import listMutations from './list/mutations';
import listState from './list/state';
import * as listGetters from './list/getters';
import * as detailsActions from './details/actions';
import detailsMutations from './details/mutations';
import detailsState from './details/state';
import * as detailsGetters from './details/getters';
Vue.use(Vuex); Vue.use(Vuex);
export const createStore = () => export const createStore = () =>
new Vuex.Store({ new Vuex.Store({
state: { modules: {
errors: [], list: {
externalUrl: '', namespaced: true,
loading: true, state: listState(),
actions: listActions,
mutations: listMutations,
getters: listGetters,
},
details: {
namespaced: true,
state: detailsState(),
actions: detailsActions,
mutations: detailsMutations,
getters: detailsGetters,
},
}, },
actions,
mutations,
getters,
}); });
export default createStore(); export default createStore();
import Service from '../services'; import Service from '../../services';
import * as types from './mutation_types'; import * as types from './mutation_types';
import createFlash from '~/flash'; import createFlash from '~/flash';
import Poll from '~/lib/utils/poll'; import Poll from '~/lib/utils/poll';
...@@ -9,7 +9,7 @@ let eTagPoll; ...@@ -9,7 +9,7 @@ let eTagPoll;
export function startPolling({ commit, dispatch }, endpoint) { export function startPolling({ commit, dispatch }, endpoint) {
eTagPoll = new Poll({ eTagPoll = new Poll({
resource: Service, resource: Service,
method: 'getErrorList', method: 'getSentryData',
data: { endpoint }, data: { endpoint },
successCallback: ({ data }) => { successCallback: ({ data }) => {
if (!data) { if (!data) {
......
export default () => ({
errors: [],
externalUrl: '',
loading: true,
});
import ErrorTrackingDetails from '~/error_tracking/details';
document.addEventListener('DOMContentLoaded', () => {
ErrorTrackingDetails();
});
import ErrorTracking from '~/error_tracking'; import ErrorTrackingList from '~/error_tracking/list';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
ErrorTracking(); ErrorTrackingList();
}); });
.error-details {
li {
@include gl-line-height-32;
}
}
.stacktrace {
.file-title {
svg {
vertical-align: middle;
top: -1px;
}
}
.line_content.old::before {
content: none !important;
}
}
- page_title _('Error Details') - page_title _('Error Details')
- add_to_breadcrumbs 'Errors', project_error_tracking_index_path(@project)
#js-error_tracking{ data: error_details_data(@current_user, @project) } #js-error_details{ data: error_details_data(@current_user, @project) }
---
title: Detail view of Sentry error in GitLab
merge_request: 18878
author:
type: added
---
title: Sentry error stacktrace
merge_request: 19492
author:
type: added
...@@ -6625,6 +6625,9 @@ msgstr "" ...@@ -6625,6 +6625,9 @@ msgstr ""
msgid "Error deleting %{issuableType}" msgid "Error deleting %{issuableType}"
msgstr "" msgstr ""
msgid "Error details"
msgstr ""
msgid "Error fetching diverging counts for branches. Please try again." msgid "Error fetching diverging counts for branches. Please try again."
msgstr "" msgstr ""
...@@ -7057,6 +7060,9 @@ msgstr "" ...@@ -7057,6 +7060,9 @@ msgstr ""
msgid "Failed to load emoji list." msgid "Failed to load emoji list."
msgstr "" msgstr ""
msgid "Failed to load error details from Sentry."
msgstr ""
msgid "Failed to load errors from Sentry. Error message: %{errorMessage}" msgid "Failed to load errors from Sentry. Error message: %{errorMessage}"
msgstr "" msgstr ""
...@@ -7066,6 +7072,9 @@ msgstr "" ...@@ -7066,6 +7072,9 @@ msgstr ""
msgid "Failed to load related branches" msgid "Failed to load related branches"
msgstr "" msgstr ""
msgid "Failed to load stacktrace."
msgstr ""
msgid "Failed to mark this issue as a duplicate because referenced issue was not found." msgid "Failed to mark this issue as a duplicate because referenced issue was not found."
msgstr "" msgstr ""
...@@ -7455,6 +7464,9 @@ msgstr "" ...@@ -7455,6 +7464,9 @@ msgstr ""
msgid "First name" msgid "First name"
msgstr "" msgstr ""
msgid "First seen"
msgstr ""
msgid "Fixed date" msgid "Fixed date"
msgstr "" msgstr ""
...@@ -14222,6 +14234,9 @@ msgstr "" ...@@ -14222,6 +14234,9 @@ msgstr ""
msgid "Report abuse to admin" msgid "Report abuse to admin"
msgstr "" msgstr ""
msgid "Reported %{timeAgo} by %{reportedBy}"
msgstr ""
msgid "Reporting" msgid "Reporting"
msgstr "" msgstr ""
...@@ -15198,6 +15213,9 @@ msgstr "" ...@@ -15198,6 +15213,9 @@ msgstr ""
msgid "Sentry API URL" msgid "Sentry API URL"
msgstr "" msgstr ""
msgid "Sentry event"
msgstr ""
msgid "Sep" msgid "Sep"
msgstr "" msgstr ""
...@@ -16022,6 +16040,9 @@ msgstr "" ...@@ -16022,6 +16040,9 @@ msgstr ""
msgid "Squash commits" msgid "Squash commits"
msgstr "" msgstr ""
msgid "Stack trace"
msgstr ""
msgid "Stage" msgid "Stage"
msgstr "" msgstr ""
......
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlLoadingIcon, GlLink } from '@gitlab/ui';
import Stacktrace from '~/error_tracking/components/stacktrace.vue';
import ErrorDetails from '~/error_tracking/components/error_details.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('ErrorDetails', () => {
let store;
let wrapper;
let actions;
let getters;
function mountComponent() {
wrapper = shallowMount(ErrorDetails, {
localVue,
store,
propsData: {
issueDetailsPath: '/123/details',
issueStackTracePath: '/stacktrace',
},
});
}
beforeEach(() => {
actions = {
startPollingDetails: () => {},
startPollingStacktrace: () => {},
};
getters = {
sentryUrl: () => 'sentry.io',
stacktrace: () => [{ context: [1, 2], lineNo: 53, filename: 'index.js' }],
};
const state = {
error: {},
loading: true,
stacktraceData: {},
loadingStacktrace: true,
};
store = new Vuex.Store({
modules: {
details: {
namespaced: true,
actions,
state,
getters,
},
},
});
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
describe('loading', () => {
beforeEach(() => {
mountComponent();
});
it('should show spinner while loading', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.find(GlLink).exists()).toBe(false);
expect(wrapper.find(Stacktrace).exists()).toBe(false);
});
});
describe('Error details', () => {
it('should show Sentry error details without stacktrace', () => {
store.state.details.loading = false;
store.state.details.error.id = 1;
mountComponent();
expect(wrapper.find(GlLink).exists()).toBe(true);
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.find(Stacktrace).exists()).toBe(false);
});
describe('Stacktrace', () => {
it('should show stacktrace', () => {
store.state.details.loading = false;
store.state.details.error.id = 1;
store.state.details.loadingStacktrace = false;
mountComponent();
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find(Stacktrace).exists()).toBe(true);
});
it('should NOT show stacktrace if no entries', () => {
store.state.details.loading = false;
store.state.details.loadingStacktrace = false;
store.getters = { 'details/sentryUrl': () => 'sentry.io', 'details/stacktrace': () => [] };
mountComponent();
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find(Stacktrace).exists()).toBe(false);
});
});
});
});
...@@ -34,7 +34,7 @@ describe('ErrorTrackingList', () => { ...@@ -34,7 +34,7 @@ describe('ErrorTrackingList', () => {
beforeEach(() => { beforeEach(() => {
actions = { actions = {
getErrorList: () => {}, getSentryData: () => {},
startPolling: () => {}, startPolling: () => {},
restartPolling: jest.fn().mockName('restartPolling'), restartPolling: jest.fn().mockName('restartPolling'),
}; };
...@@ -45,8 +45,13 @@ describe('ErrorTrackingList', () => { ...@@ -45,8 +45,13 @@ describe('ErrorTrackingList', () => {
}; };
store = new Vuex.Store({ store = new Vuex.Store({
actions, modules: {
state, list: {
namespaced: true,
actions,
state,
},
},
}); });
}); });
...@@ -70,7 +75,7 @@ describe('ErrorTrackingList', () => { ...@@ -70,7 +75,7 @@ describe('ErrorTrackingList', () => {
describe('results', () => { describe('results', () => {
beforeEach(() => { beforeEach(() => {
store.state.loading = false; store.state.list.loading = false;
mountComponent(); mountComponent();
}); });
...@@ -84,7 +89,7 @@ describe('ErrorTrackingList', () => { ...@@ -84,7 +89,7 @@ describe('ErrorTrackingList', () => {
describe('no results', () => { describe('no results', () => {
beforeEach(() => { beforeEach(() => {
store.state.loading = false; store.state.list.loading = false;
mountComponent(); mountComponent();
}); });
......
import { shallowMount } from '@vue/test-utils';
import StackTraceEntry from '~/error_tracking/components/stacktrace_entry.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
describe('Stacktrace Entry', () => {
let wrapper;
function mountComponent(props) {
wrapper = shallowMount(StackTraceEntry, {
propsData: {
filePath: 'sidekiq/util.rb',
lines: [
[22, ' def safe_thread(name, \u0026block)\n'],
[23, ' Thread.new do\n'],
[24, " Thread.current['sidekiq_label'] = name\n"],
[25, ' watchdog(name, \u0026block)\n'],
],
errorLine: 24,
...props,
},
});
}
beforeEach(() => {
mountComponent();
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
it('should render stacktrace entry collapsed', () => {
expect(wrapper.find(StackTraceEntry).exists()).toBe(true);
expect(wrapper.find(ClipboardButton).exists()).toBe(true);
expect(wrapper.find(Icon).exists()).toBe(true);
expect(wrapper.find(FileIcon).exists()).toBe(true);
expect(wrapper.element.querySelectorAll('table').length).toBe(0);
});
it('should render stacktrace entry table expanded', () => {
mountComponent({ expanded: true });
expect(wrapper.element.querySelectorAll('tr.line_holder').length).toBe(4);
expect(wrapper.element.querySelectorAll('.line_content.old').length).toBe(1);
});
});
import { shallowMount } from '@vue/test-utils';
import Stacktrace from '~/error_tracking/components/stacktrace.vue';
import StackTraceEntry from '~/error_tracking/components/stacktrace_entry.vue';
describe('ErrorDetails', () => {
let wrapper;
const stackTraceEntry = {
filename: 'sidekiq/util.rb',
context: [
[22, ' def safe_thread(name, \u0026block)\n'],
[23, ' Thread.new do\n'],
[24, " Thread.current['sidekiq_label'] = name\n"],
[25, ' watchdog(name, \u0026block)\n'],
],
lineNo: 24,
};
function mountComponent(entries) {
wrapper = shallowMount(Stacktrace, {
propsData: {
entries,
},
});
}
describe('Stacktrace', () => {
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
it('should render single Stacktrace entry', () => {
mountComponent([stackTraceEntry]);
expect(wrapper.findAll(StackTraceEntry).length).toBe(1);
});
it('should render multiple Stacktrace entry', () => {
const entriesNum = 3;
mountComponent(new Array(entriesNum).fill(stackTraceEntry));
expect(wrapper.findAll(StackTraceEntry).length).toBe(entriesNum);
});
});
});
import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import createFlash from '~/flash';
import * as actions from '~/error_tracking/store/details/actions';
import * as types from '~/error_tracking/store/details/mutation_types';
jest.mock('~/flash.js');
let mock;
describe('Sentry error details store actions', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
createFlash.mockClear();
});
describe('startPollingDetails', () => {
const endpoint = '123/details';
it('should commit SET_ERROR with received response', done => {
const payload = { error: { id: 1 } };
mock.onGet().reply(200, payload);
testAction(
actions.startPollingDetails,
{ endpoint },
{},
[
{ type: types.SET_ERROR, payload: payload.error },
{ type: types.SET_LOADING, payload: false },
],
[],
() => {
done();
},
);
});
it('should show flash on API error', done => {
mock.onGet().reply(400);
testAction(
actions.startPollingDetails,
{ endpoint },
{},
[{ type: types.SET_LOADING, payload: false }],
[],
() => {
expect(createFlash).toHaveBeenCalledTimes(1);
done();
},
);
});
});
describe('startPollingStacktrace', () => {
const endpoint = '123/stacktrace';
it('should commit SET_ERROR with received response', done => {
const payload = { error: [1, 2, 3] };
mock.onGet().reply(200, payload);
testAction(
actions.startPollingStacktrace,
{ endpoint },
{},
[
{ type: types.SET_STACKTRACE_DATA, payload: payload.error },
{ type: types.SET_LOADING_STACKTRACE, payload: false },
],
[],
() => {
done();
},
);
});
it('should show flash on API error', done => {
mock.onGet().reply(400);
testAction(
actions.startPollingStacktrace,
{ endpoint },
{},
[{ type: types.SET_LOADING_STACKTRACE, payload: false }],
[],
() => {
expect(createFlash).toHaveBeenCalledTimes(1);
done();
},
);
});
});
});
import * as getters from '~/error_tracking/store/details/getters';
describe('Sentry error details store getters', () => {
const state = {
stacktraceData: { stack_trace_entries: [1, 2] },
};
describe('stacktrace', () => {
it('should get stacktrace', () => {
expect(getters.stacktrace(state)).toEqual([2, 1]);
});
});
});
import * as getters from '~/error_tracking/store/getters'; import * as getters from '~/error_tracking/store/list/getters';
describe('Error Tracking getters', () => { describe('Error Tracking getters', () => {
let state; let state;
......
import mutations from '~/error_tracking/store/mutations'; import mutations from '~/error_tracking/store/list/mutations';
import * as types from '~/error_tracking/store/mutation_types'; import * as types from '~/error_tracking/store/list/mutation_types';
describe('Error tracking mutations', () => { describe('Error tracking mutations', () => {
describe('SET_ERRORS', () => { describe('SET_ERRORS', () => {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment