Commit 3c6e0560 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '39979-grapql-for-error-details' into 'master'

Use GraphQL to load error tracking detail page content

See merge request gitlab-org/gitlab!22422
parents afa09780 fe22a870
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import dateFormat from 'dateformat';
import createFlash from '~/flash';
import { GlFormInput, GlLink, GlLoadingIcon, GlBadge } from '@gitlab/ui';
import { __, sprintf, n__ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
......@@ -11,6 +12,8 @@ import TrackEventDirective from '~/vue_shared/directives/track_event';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { trackClickErrorLinkToSentryOptions } from '../utils';
import query from '../queries/details.query.graphql';
export default {
components: {
LoadingButton,
......@@ -27,6 +30,14 @@ export default {
},
mixins: [timeagoMixin],
props: {
issueId: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
issueDetailsPath: {
type: String,
required: true,
......@@ -44,8 +55,28 @@ export default {
required: true,
},
},
apollo: {
GQLerror: {
query,
variables() {
return {
fullPath: this.projectPath,
errorId: `gid://gitlab/Gitlab::ErrorTracking::DetailedError/${this.issueId}`,
};
},
pollInterval: 2000,
update: data => data.project.sentryDetailedError,
error: () => createFlash(__('Failed to load error details from Sentry.')),
result(res) {
if (res.data.project?.sentryDetailedError) {
this.$apollo.queries.GQLerror.stopPolling();
}
},
},
},
data() {
return {
GQLerror: null,
issueCreationInProgress: false,
};
},
......@@ -56,26 +87,28 @@ export default {
return sprintf(
__('Reported %{timeAgo} by %{reportedBy}'),
{
reportedBy: `<strong>${this.error.culprit}</strong>`,
reportedBy: `<strong>${this.GQLerror.culprit}</strong>`,
timeAgo: this.timeFormatted(this.stacktraceData.date_received),
},
false,
);
},
firstReleaseLink() {
return `${this.error.external_base_url}/releases/${this.error.first_release_short_version}`;
return `${this.error.external_base_url}/releases/${this.GQLerror.firstReleaseShortVersion}`;
},
lastReleaseLink() {
return `${this.error.external_base_url}releases/${this.error.last_release_short_version}`;
return `${this.error.external_base_url}releases/${this.GQLerror.lastReleaseShortVersion}`;
},
showDetails() {
return Boolean(!this.loading && this.error && this.error.id);
return Boolean(
!this.loading && !this.$apollo.queries.GQLerror.loading && this.error && this.GQLerror,
);
},
showStacktrace() {
return Boolean(!this.loadingStacktrace && this.stacktrace && this.stacktrace.length);
},
issueTitle() {
return this.error.title;
return this.GQLerror.title;
},
issueDescription() {
return sprintf(
......@@ -84,13 +117,13 @@ export default {
),
{
description: '# Error Details:\n',
errorUrl: `${this.error.external_url}\n`,
firstSeen: `\n${this.error.first_seen}\n`,
lastSeen: `${this.error.last_seen}\n`,
countLabel: n__('- Event', '- Events', this.error.count),
count: `${this.error.count}\n`,
userCountLabel: n__('- User', '- Users', this.error.user_count),
userCount: `${this.error.user_count}\n`,
errorUrl: `${this.GQLerror.externalUrl}\n`,
firstSeen: `\n${this.GQLerror.firstSeen}\n`,
lastSeen: `${this.GQLerror.lastSeen}\n`,
countLabel: n__('- Event', '- Events', this.GQLerror.count),
count: `${this.GQLerror.count}\n`,
userCountLabel: n__('- User', '- Users', this.GQLerror.userCount),
userCount: `${this.GQLerror.userCount}\n`,
},
false,
);
......@@ -119,7 +152,7 @@ export default {
<template>
<div>
<div v-if="loading" class="py-3">
<div v-if="$apollo.queries.GQLerror.loading || loading" class="py-3">
<gl-loading-icon :size="3" />
</div>
<div v-else-if="showDetails" class="error-details">
......@@ -129,7 +162,7 @@ export default {
<gl-form-input class="hidden" name="issue[title]" :value="issueTitle" />
<input name="issue[description]" :value="issueDescription" type="hidden" />
<gl-form-input
:value="error.id"
:value="GQLerror.id"
class="hidden"
name="issue[sentry_issue_attributes][sentry_issue_identifier]"
/>
......@@ -145,16 +178,16 @@ export default {
</form>
</div>
<div>
<tooltip-on-truncate :title="error.title" truncate-target="child" placement="top">
<h2 class="text-truncate">{{ error.title }}</h2>
<tooltip-on-truncate :title="GQLerror.title" truncate-target="child" placement="top">
<h2 class="text-truncate">{{ GQLerror.title }}</h2>
</tooltip-on-truncate>
<template v-if="error.tags">
<gl-badge v-if="error.tags.level" variant="danger" class="rounded-pill mr-2">{{
errorLevel
}}</gl-badge>
<gl-badge v-if="error.tags.logger" variant="light" class="rounded-pill">{{
error.tags.logger
}}</gl-badge>
<gl-badge v-if="error.tags.level" variant="danger" class="rounded-pill mr-2"
>{{ errorLevel }}
</gl-badge>
<gl-badge v-if="error.tags.logger" variant="light" class="rounded-pill"
>{{ error.tags.logger }}
</gl-badge>
</template>
<h3>{{ __('Error details') }}</h3>
......@@ -168,35 +201,35 @@ export default {
<li>
<span class="bold">{{ __('Sentry event') }}:</span>
<gl-link
v-track-event="trackClickErrorLinkToSentryOptions(error.external_url)"
:href="error.external_url"
v-track-event="trackClickErrorLinkToSentryOptions(GQLerror.externalUrl)"
:href="GQLerror.externalUrl"
target="_blank"
>
<span class="text-truncate">{{ error.external_url }}</span>
<span class="text-truncate">{{ GQLerror.externalUrl }}</span>
<icon name="external-link" class="ml-1 flex-shrink-0" />
</gl-link>
</li>
<li v-if="error.first_release_short_version">
<li v-if="GQLerror.firstReleaseShortVersion">
<span class="bold">{{ __('First seen') }}:</span>
{{ formatDate(error.first_seen) }}
{{ formatDate(GQLerror.firstSeen) }}
<gl-link :href="firstReleaseLink" target="_blank">
<span>{{ __('Release') }}: {{ error.first_release_short_version }}</span>
<span>{{ __('Release') }}: {{ GQLerror.firstReleaseShortVersion }}</span>
</gl-link>
</li>
<li v-if="error.last_release_short_version">
<li v-if="GQLerror.lastReleaseShortVersion">
<span class="bold">{{ __('Last seen') }}:</span>
{{ formatDate(error.last_seen) }}
{{ formatDate(GQLerror.lastSeen) }}
<gl-link :href="lastReleaseLink" target="_blank">
<span>{{ __('Release') }}: {{ error.last_release_short_version }}</span>
<span>{{ __('Release') }}: {{ GQLerror.lastReleaseShortVersion }}</span>
</gl-link>
</li>
<li>
<span class="bold">{{ __('Events') }}:</span>
<span>{{ error.count }}</span>
<span>{{ GQLerror.count }}</span>
</li>
<li>
<span class="bold">{{ __('Users') }}:</span>
<span>{{ error.user_count }}</span>
<span>{{ GQLerror.userCount }}</span>
</li>
</ul>
......
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import store from './store';
import ErrorDetails from './components/error_details.vue';
import csrf from '~/lib/utils/csrf';
Vue.use(VueApollo);
export default () => {
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
// eslint-disable-next-line no-new
new Vue({
el: '#js-error_details',
apolloProvider,
components: {
ErrorDetails,
},
store,
render(createElement) {
const domEl = document.querySelector(this.$options.el);
const { issueDetailsPath, issueStackTracePath, projectIssuesPath } = domEl.dataset;
const {
issueId,
projectPath,
issueDetailsPath,
issueStackTracePath,
projectIssuesPath,
} = domEl.dataset;
return createElement('error-details', {
props: {
issueId,
projectPath,
issueDetailsPath,
issueStackTracePath,
projectIssuesPath,
......
query errorDetails($fullPath: ID!, $errorId: ID!) {
project(fullPath: $fullPath) {
sentryDetailedError(id: $errorId) {
id
sentryId
title
userCount
count
firstSeen
lastSeen
message
culprit
externalUrl
firstReleaseShortVersion
lastReleaseShortVersion
}
}
}
......@@ -18,9 +18,11 @@ module Projects::ErrorTrackingHelper
opts = [project, issue_id, { format: :json }]
{
'project-issues-path' => project_issues_path(project),
'issue-id' => issue_id,
'project-path' => project.full_path,
'issue-details-path' => details_project_error_tracking_index_path(*opts),
'issue-update-path' => update_project_error_tracking_index_path(*opts),
'project-issues-path' => project_issues_path(project),
'issue-stack-trace-path' => stack_trace_project_error_tracking_index_path(*opts)
}
end
......
---
title: Use GraphQL to load error tracking detail page content
merge_request: 22422
author:
type: performance
......@@ -13,6 +13,7 @@ describe('ErrorDetails', () => {
let wrapper;
let actions;
let getters;
let mocks;
const findInput = name => {
const inputs = wrapper.findAll(GlFormInput).filter(c => c.attributes('name') === name);
......@@ -24,13 +25,27 @@ describe('ErrorDetails', () => {
stubs: { LoadingButton },
localVue,
store,
mocks,
propsData: {
issueId: '123',
projectPath: '/root/gitlab-test',
issueDetailsPath: '/123/details',
issueStackTracePath: '/stacktrace',
projectIssuesPath: '/test-project/issues/',
csrfToken: 'fakeToken',
},
});
wrapper.setData({
GQLerror: {
id: 129381,
title: 'Issue title',
externalUrl: 'http://sentry.gitlab.net/gitlab',
firstSeen: '2017-05-26T13:32:48Z',
lastSeen: '2018-05-26T13:32:48Z',
count: 12,
userCount: 2,
},
});
}
beforeEach(() => {
......@@ -61,6 +76,19 @@ describe('ErrorDetails', () => {
},
},
});
const query = jest.fn();
mocks = {
$apollo: {
query,
queries: {
GQLerror: {
loading: true,
stopPolling: jest.fn(),
},
},
},
};
});
afterEach(() => {
......@@ -85,10 +113,11 @@ describe('ErrorDetails', () => {
beforeEach(() => {
store.state.details.loading = false;
store.state.details.error.id = 1;
mocks.$apollo.queries.GQLerror.loading = false;
mountComponent();
});
it('should show Sentry error details without stacktrace', () => {
mountComponent();
expect(wrapper.find(GlLink).exists()).toBe(true);
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.find(Stacktrace).exists()).toBe(false);
......@@ -99,13 +128,17 @@ describe('ErrorDetails', () => {
it('should show language and error level badges', () => {
store.state.details.error.tags = { level: 'error', logger: 'ruby' };
mountComponent();
expect(wrapper.findAll(GlBadge).length).toBe(2);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.findAll(GlBadge).length).toBe(2);
});
});
it('should NOT show the badge if the tag is not present', () => {
store.state.details.error.tags = { level: 'error' };
mountComponent();
expect(wrapper.findAll(GlBadge).length).toBe(1);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.findAll(GlBadge).length).toBe(1);
});
});
});
......@@ -113,8 +146,10 @@ describe('ErrorDetails', () => {
it('should show stacktrace', () => {
store.state.details.loadingStacktrace = false;
mountComponent();
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find(Stacktrace).exists()).toBe(true);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find(Stacktrace).exists()).toBe(true);
});
});
it('should NOT show stacktrace if no entries', () => {
......@@ -128,15 +163,6 @@ describe('ErrorDetails', () => {
describe('When a user clicks the create issue button', () => {
beforeEach(() => {
store.state.details.error = {
id: 129381,
title: 'Issue title',
external_url: 'http://sentry.gitlab.net/gitlab',
first_seen: '2017-05-26T13:32:48Z',
last_seen: '2018-05-26T13:32:48Z',
count: 12,
user_count: 2,
};
mountComponent();
});
......
......@@ -80,11 +80,20 @@ describe Projects::ErrorTrackingHelper do
let(:issue_id) { 1234 }
let(:route_params) { [project.owner, project, issue_id, { format: :json }] }
let(:details_path) { details_namespace_project_error_tracking_index_path(*route_params) }
let(:project_path) { project.full_path }
let(:stack_trace_path) { stack_trace_namespace_project_error_tracking_index_path(*route_params) }
let(:issues_path) { project_issues_path(project) }
let(:result) { helper.error_details_data(project, issue_id) }
it 'returns the correct issue id' do
expect(result['issue-id']).to eq issue_id
end
it 'returns the correct project path' do
expect(result['project-path']).to eq project_path
end
it 'returns the correct details path' do
expect(result['issue-details-path']).to eq details_path
end
......
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