summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue141
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue17
-rw-r--r--app/assets/javascripts/error_tracking/components/stacktrace.vue33
-rw-r--r--app/assets/javascripts/error_tracking/components/stacktrace_entry.vue110
-rw-r--r--app/assets/javascripts/error_tracking/details.js25
-rw-r--r--app/assets/javascripts/error_tracking/list.js (renamed from app/assets/javascripts/error_tracking/index.js)0
-rw-r--r--app/assets/javascripts/error_tracking/services/index.js2
-rw-r--r--app/assets/javascripts/error_tracking/store/details/actions.js63
-rw-r--r--app/assets/javascripts/error_tracking/store/details/getters.js3
-rw-r--r--app/assets/javascripts/error_tracking/store/details/mutation_types.js4
-rw-r--r--app/assets/javascripts/error_tracking/store/details/mutations.js16
-rw-r--r--app/assets/javascripts/error_tracking/store/details/state.js6
-rw-r--r--app/assets/javascripts/error_tracking/store/index.js35
-rw-r--r--app/assets/javascripts/error_tracking/store/list/actions.js (renamed from app/assets/javascripts/error_tracking/store/actions.js)4
-rw-r--r--app/assets/javascripts/error_tracking/store/list/getters.js (renamed from app/assets/javascripts/error_tracking/store/getters.js)0
-rw-r--r--app/assets/javascripts/error_tracking/store/list/mutation_types.js (renamed from app/assets/javascripts/error_tracking/store/mutation_types.js)0
-rw-r--r--app/assets/javascripts/error_tracking/store/list/mutations.js (renamed from app/assets/javascripts/error_tracking/store/mutations.js)0
-rw-r--r--app/assets/javascripts/error_tracking/store/list/state.js5
-rw-r--r--app/assets/javascripts/pages/projects/error_tracking/details/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/error_tracking/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/error_tracking/index/index.js5
-rw-r--r--app/assets/stylesheets/pages/error_details.scss18
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;
+ }
+}