diff options
Diffstat (limited to 'app/assets')
22 files changed, 471 insertions, 26 deletions
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue new file mode 100644 index 00000000000..37c9818f869 --- /dev/null +++ b/app/assets/javascripts/error_tracking/components/error_details.vue @@ -0,0 +1,141 @@ +<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> diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue index a76eb747c34..88139ce7403 100644 --- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue +++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue @@ -8,11 +8,12 @@ import { GlTable, GlSearchBoxByType, } from '@gitlab/ui'; +import { visitUrl } from '~/lib/utils/url_utility'; import Icon from '~/vue_shared/components/icon.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import { __ } from '~/locale'; import TrackEventDirective from '~/vue_shared/directives/track_event'; -import { trackViewInSentryOptions, trackClickErrorLinkToSentryOptions } from '../utils'; +import { trackViewInSentryOptions } from '../utils'; export default { fields: [ @@ -62,8 +63,8 @@ export default { }; }, computed: { - ...mapState(['errors', 'externalUrl', 'loading']), - ...mapGetters(['filterErrorsByTitle']), + ...mapState('list', ['errors', 'externalUrl', 'loading']), + ...mapGetters('list', ['filterErrorsByTitle']), filteredErrors() { return this.errorSearchQuery ? this.filterErrorsByTitle(this.errorSearchQuery) : this.errors; }, @@ -74,9 +75,11 @@ export default { } }, methods: { - ...mapActions(['startPolling', 'restartPolling']), + ...mapActions('list', ['startPolling', 'restartPolling']), trackViewInSentryOptions, - trackClickErrorLinkToSentryOptions, + viewDetails(errorId) { + visitUrl(`error_tracking/${errorId}/details`); + }, }, }; </script> @@ -125,13 +128,11 @@ export default { <template slot="error" slot-scope="errors"> <div class="d-flex flex-column"> <gl-link - v-track-event="trackClickErrorLinkToSentryOptions(errors.item.externalUrl)" - :href="errors.item.externalUrl" class="d-flex text-dark" target="_blank" + @click="viewDetails(errors.item.id)" > <strong class="text-truncate">{{ errors.item.title.trim() }}</strong> - <icon name="external-link" class="ml-1 flex-shrink-0" /> </gl-link> <span class="text-secondary text-truncate"> {{ errors.item.culprit }} diff --git a/app/assets/javascripts/error_tracking/components/stacktrace.vue b/app/assets/javascripts/error_tracking/components/stacktrace.vue new file mode 100644 index 00000000000..6b71967624f --- /dev/null +++ b/app/assets/javascripts/error_tracking/components/stacktrace.vue @@ -0,0 +1,33 @@ +<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> diff --git a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue new file mode 100644 index 00000000000..ad542c579a9 --- /dev/null +++ b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue @@ -0,0 +1,110 @@ +<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> diff --git a/app/assets/javascripts/error_tracking/details.js b/app/assets/javascripts/error_tracking/details.js new file mode 100644 index 00000000000..b9b51a6539f --- /dev/null +++ b/app/assets/javascripts/error_tracking/details.js @@ -0,0 +1,25 @@ +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, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/error_tracking/index.js b/app/assets/javascripts/error_tracking/list.js index 073e2c8f1c7..073e2c8f1c7 100644 --- a/app/assets/javascripts/error_tracking/index.js +++ b/app/assets/javascripts/error_tracking/list.js diff --git a/app/assets/javascripts/error_tracking/services/index.js b/app/assets/javascripts/error_tracking/services/index.js index ab89521dc46..68988296cc2 100644 --- a/app/assets/javascripts/error_tracking/services/index.js +++ b/app/assets/javascripts/error_tracking/services/index.js @@ -1,7 +1,7 @@ import axios from '~/lib/utils/axios_utils'; export default { - getErrorList({ endpoint }) { + getSentryData({ endpoint }) { return axios.get(endpoint); }, }; diff --git a/app/assets/javascripts/error_tracking/store/details/actions.js b/app/assets/javascripts/error_tracking/store/details/actions.js new file mode 100644 index 00000000000..0390bca7175 --- /dev/null +++ b/app/assets/javascripts/error_tracking/store/details/actions.js @@ -0,0 +1,63 @@ +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 () => {}; diff --git a/app/assets/javascripts/error_tracking/store/details/getters.js b/app/assets/javascripts/error_tracking/store/details/getters.js new file mode 100644 index 00000000000..7d13439d721 --- /dev/null +++ b/app/assets/javascripts/error_tracking/store/details/getters.js @@ -0,0 +1,3 @@ +export const stacktrace = state => state.stacktraceData.stack_trace_entries.reverse(); + +export default () => {}; diff --git a/app/assets/javascripts/error_tracking/store/details/mutation_types.js b/app/assets/javascripts/error_tracking/store/details/mutation_types.js new file mode 100644 index 00000000000..a2592253a2d --- /dev/null +++ b/app/assets/javascripts/error_tracking/store/details/mutation_types.js @@ -0,0 +1,4 @@ +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'; diff --git a/app/assets/javascripts/error_tracking/store/details/mutations.js b/app/assets/javascripts/error_tracking/store/details/mutations.js new file mode 100644 index 00000000000..6f4720444e0 --- /dev/null +++ b/app/assets/javascripts/error_tracking/store/details/mutations.js @@ -0,0 +1,16 @@ +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; + }, +}; diff --git a/app/assets/javascripts/error_tracking/store/details/state.js b/app/assets/javascripts/error_tracking/store/details/state.js new file mode 100644 index 00000000000..95fb0ba0558 --- /dev/null +++ b/app/assets/javascripts/error_tracking/store/details/state.js @@ -0,0 +1,6 @@ +export default () => ({ + error: {}, + stacktraceData: {}, + loading: true, + loadingStacktrace: true, +}); diff --git a/app/assets/javascripts/error_tracking/store/index.js b/app/assets/javascripts/error_tracking/store/index.js index 3ba05e22727..941c752e96a 100644 --- a/app/assets/javascripts/error_tracking/store/index.js +++ b/app/assets/javascripts/error_tracking/store/index.js @@ -1,21 +1,36 @@ import Vue from 'vue'; import Vuex from 'vuex'; -import * as actions from './actions'; -import * as getters from './getters'; -import mutations from './mutations'; + +import * as listActions from './list/actions'; +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); export const createStore = () => new Vuex.Store({ - state: { - errors: [], - externalUrl: '', - loading: true, + modules: { + list: { + namespaced: 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(); diff --git a/app/assets/javascripts/error_tracking/store/actions.js b/app/assets/javascripts/error_tracking/store/list/actions.js index 1e754a4f54f..18c6e5e9695 100644 --- a/app/assets/javascripts/error_tracking/store/actions.js +++ b/app/assets/javascripts/error_tracking/store/list/actions.js @@ -1,4 +1,4 @@ -import Service from '../services'; +import Service from '../../services'; import * as types from './mutation_types'; import createFlash from '~/flash'; import Poll from '~/lib/utils/poll'; @@ -9,7 +9,7 @@ let eTagPoll; export function startPolling({ commit, dispatch }, endpoint) { eTagPoll = new Poll({ resource: Service, - method: 'getErrorList', + method: 'getSentryData', data: { endpoint }, successCallback: ({ data }) => { if (!data) { diff --git a/app/assets/javascripts/error_tracking/store/getters.js b/app/assets/javascripts/error_tracking/store/list/getters.js index 1a2ec62f79f..1a2ec62f79f 100644 --- a/app/assets/javascripts/error_tracking/store/getters.js +++ b/app/assets/javascripts/error_tracking/store/list/getters.js diff --git a/app/assets/javascripts/error_tracking/store/mutation_types.js b/app/assets/javascripts/error_tracking/store/list/mutation_types.js index f9d77a6b08e..f9d77a6b08e 100644 --- a/app/assets/javascripts/error_tracking/store/mutation_types.js +++ b/app/assets/javascripts/error_tracking/store/list/mutation_types.js diff --git a/app/assets/javascripts/error_tracking/store/mutations.js b/app/assets/javascripts/error_tracking/store/list/mutations.js index e4bd81db9c9..e4bd81db9c9 100644 --- a/app/assets/javascripts/error_tracking/store/mutations.js +++ b/app/assets/javascripts/error_tracking/store/list/mutations.js diff --git a/app/assets/javascripts/error_tracking/store/list/state.js b/app/assets/javascripts/error_tracking/store/list/state.js new file mode 100644 index 00000000000..d371350ef0e --- /dev/null +++ b/app/assets/javascripts/error_tracking/store/list/state.js @@ -0,0 +1,5 @@ +export default () => ({ + errors: [], + externalUrl: '', + loading: true, +}); diff --git a/app/assets/javascripts/pages/projects/error_tracking/details/index.js b/app/assets/javascripts/pages/projects/error_tracking/details/index.js new file mode 100644 index 00000000000..25d1c744e1b --- /dev/null +++ b/app/assets/javascripts/pages/projects/error_tracking/details/index.js @@ -0,0 +1,5 @@ +import ErrorTrackingDetails from '~/error_tracking/details'; + +document.addEventListener('DOMContentLoaded', () => { + ErrorTrackingDetails(); +}); diff --git a/app/assets/javascripts/pages/projects/error_tracking/index.js b/app/assets/javascripts/pages/projects/error_tracking/index.js deleted file mode 100644 index 5a8fe137e9a..00000000000 --- a/app/assets/javascripts/pages/projects/error_tracking/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import ErrorTracking from '~/error_tracking'; - -document.addEventListener('DOMContentLoaded', () => { - ErrorTracking(); -}); diff --git a/app/assets/javascripts/pages/projects/error_tracking/index/index.js b/app/assets/javascripts/pages/projects/error_tracking/index/index.js new file mode 100644 index 00000000000..ead81cd5d2d --- /dev/null +++ b/app/assets/javascripts/pages/projects/error_tracking/index/index.js @@ -0,0 +1,5 @@ +import ErrorTrackingList from '~/error_tracking/list'; + +document.addEventListener('DOMContentLoaded', () => { + ErrorTrackingList(); +}); diff --git a/app/assets/stylesheets/pages/error_details.scss b/app/assets/stylesheets/pages/error_details.scss new file mode 100644 index 00000000000..0515db914e9 --- /dev/null +++ b/app/assets/stylesheets/pages/error_details.scss @@ -0,0 +1,18 @@ +.error-details { + li { + @include gl-line-height-32; + } +} + +.stacktrace { + .file-title { + svg { + vertical-align: middle; + top: -1px; + } + } + + .line_content.old::before { + content: none !important; + } +} |