error_details.vue 11.0 KB
Newer Older
1 2 3
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import dateFormat from 'dateformat';
4
import createFlash from '~/flash';
5 6 7 8 9 10 11 12 13
import {
  GlButton,
  GlFormInput,
  GlLink,
  GlLoadingIcon,
  GlBadge,
  GlAlert,
  GlSprintf,
} from '@gitlab/ui';
14 15
import { __, sprintf, n__ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
16 17 18 19 20 21
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';
22
import { severityLevel, severityLevelVariant, errorStatus } from './constants';
23

24 25
import query from '../queries/details.query.graphql';

26 27
export default {
  components: {
28
    LoadingButton,
29
    GlButton,
30
    GlFormInput,
31 32 33 34 35
    GlLink,
    GlLoadingIcon,
    TooltipOnTruncate,
    Icon,
    Stacktrace,
36
    GlBadge,
37 38
    GlAlert,
    GlSprintf,
39 40 41 42 43 44
  },
  directives: {
    TrackEvent: TrackEventDirective,
  },
  mixins: [timeagoMixin],
  props: {
45 46 47 48 49 50 51 52 53 54 55 56
    issueUpdatePath: {
      type: String,
      required: true,
    },
    issueId: {
      type: String,
      required: true,
    },
    projectPath: {
      type: String,
      required: true,
    },
57 58 59 60
    issueStackTracePath: {
      type: String,
      required: true,
    },
61 62 63 64 65 66 67 68 69
    projectIssuesPath: {
      type: String,
      required: true,
    },
    csrfToken: {
      type: String,
      required: true,
    },
  },
70
  apollo: {
71
    error: {
72 73 74 75 76 77 78 79
      query,
      variables() {
        return {
          fullPath: this.projectPath,
          errorId: `gid://gitlab/Gitlab::ErrorTracking::DetailedError/${this.issueId}`,
        };
      },
      pollInterval: 2000,
80
      update: data => data.project.sentryErrors.detailedError,
81 82
      error: () => createFlash(__('Failed to load error details from Sentry.')),
      result(res) {
83 84 85
        if (res.data.project?.sentryErrors?.detailedError) {
          this.$apollo.queries.error.stopPolling();
          this.setStatus(this.error.status);
86 87 88 89
        }
      },
    },
  },
90 91
  data() {
    return {
92
      error: null,
93
      issueCreationInProgress: false,
94 95
      isAlertVisible: false,
      closedIssueId: null,
96
    };
97 98
  },
  computed: {
99 100 101 102 103
    ...mapState('details', [
      'loadingStacktrace',
      'stacktraceData',
      'updatingResolveStatus',
      'updatingIgnoreStatus',
104
      'errorStatus',
105
    ]),
106 107
    ...mapGetters('details', ['stacktrace']),
    firstReleaseLink() {
108
      return `${this.error.externalBaseUrl}/releases/${this.error.firstReleaseShortVersion}`;
109 110
    },
    lastReleaseLink() {
111
      return `${this.error.externalBaseUrl}/releases/${this.error.lastReleaseShortVersion}`;
112 113
    },
    showStacktrace() {
114
      return Boolean(this.stacktrace?.length);
115
    },
116
    issueTitle() {
117
      return this.error.title;
118 119 120 121 122 123 124 125
    },
    issueDescription() {
      return sprintf(
        __(
          '%{description}- Sentry event: %{errorUrl}- First seen: %{firstSeen}- Last seen: %{lastSeen} %{countLabel}: %{count}%{userCountLabel}: %{userCount}',
        ),
        {
          description: '# Error Details:\n',
126 127 128 129 130 131 132
          errorUrl: `${this.error.externalUrl}\n`,
          firstSeen: `\n${this.error.firstSeen}\n`,
          lastSeen: `${this.error.lastSeen}\n`,
          countLabel: n__('- Event', '- Events', this.error.count),
          count: `${this.error.count}\n`,
          userCountLabel: n__('- User', '- Users', this.error.userCount),
          userCount: `${this.error.userCount}\n`,
133 134 135 136
        },
        false,
      );
    },
137 138 139
    errorLevel() {
      return sprintf(__('level: %{level}'), { level: this.error.tags.level });
    },
140 141 142 143 144 145 146 147 148 149 150
    errorSeverityVariant() {
      return (
        severityLevelVariant[this.error.tags.level] || severityLevelVariant[severityLevel.ERROR]
      );
    },
    ignoreBtnLabel() {
      return this.errorStatus !== errorStatus.IGNORED ? __('Ignore') : __('Undo ignore');
    },
    resolveBtnLabel() {
      return this.errorStatus !== errorStatus.RESOLVED ? __('Resolve') : __('Unresolve');
    },
151 152 153 154 155
  },
  mounted() {
    this.startPollingStacktrace(this.issueStackTracePath);
  },
  methods: {
156 157 158 159 160 161 162
    ...mapActions('details', [
      'startPollingStacktrace',
      'updateStatus',
      'setStatus',
      'updateResolveStatus',
      'updateIgnoreStatus',
    ]),
163
    trackClickErrorLinkToSentryOptions,
164 165 166 167
    createIssue() {
      this.issueCreationInProgress = true;
      this.$refs.sentryIssueForm.submit();
    },
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
    onIgnoreStatusUpdate() {
      const status =
        this.errorStatus === errorStatus.IGNORED ? errorStatus.UNRESOLVED : errorStatus.IGNORED;
      this.updateIgnoreStatus({ endpoint: this.issueUpdatePath, status });
    },
    onResolveStatusUpdate() {
      const status =
        this.errorStatus === errorStatus.RESOLVED ? errorStatus.UNRESOLVED : errorStatus.RESOLVED;

      // eslint-disable-next-line promise/catch-or-return
      this.updateResolveStatus({ endpoint: this.issueUpdatePath, status }).then(res => {
        this.closedIssueId = res.closed_issue_iid;
        if (this.closedIssueId) {
          this.isAlertVisible = true;
        }
      });
184
    },
185
    formatDate(date) {
186
      return `${this.timeFormatted(date)} (${dateFormat(date, 'UTC:yyyy-mm-dd h:MM:ssTT Z')})`;
187 188 189 190 191 192 193
    },
  },
};
</script>

<template>
  <div>
194
    <div v-if="$apollo.queries.error.loading" class="py-3">
195 196
      <gl-loading-icon :size="3" />
    </div>
197 198 199 200 201 202 203 204 205 206 207 208 209
    <div v-else-if="error" class="error-details">
      <gl-alert v-if="isAlertVisible" @dismiss="isAlertVisible = false">
        <gl-sprintf
          :message="
            __('The associated issue #%{issueId} has been closed as the error is now resolved.')
          "
        >
          <template #issueId>
            <span>{{ closedIssueId }}</span>
          </template>
        </gl-sprintf>
      </gl-alert>

210
      <div class="top-area align-items-center justify-content-between py-3">
211 212 213 214 215 216 217 218 219 220 221
        <div v-if="!loadingStacktrace && stacktrace" data-qa-selector="reported_text">
          <gl-sprintf :message="__('Reported %{timeAgo} by %{reportedBy}')">
            <template #reportedBy>
              <strong>{{ error.culprit }}</strong>
            </template>
            <template #timeAgo>
              {{ timeFormatted(stacktraceData.date_received) }}
            </template>
          </gl-sprintf>
        </div>

222
        <div class="d-inline-flex ml-lg-auto">
223
          <loading-button
224
            :label="ignoreBtnLabel"
225
            :loading="updatingIgnoreStatus"
226 227
            data-qa-selector="update_ignore_status_button"
            @click="onIgnoreStatusUpdate"
228 229
          />
          <loading-button
230
            class="btn-outline-info ml-2"
231
            :label="resolveBtnLabel"
232
            :loading="updatingResolveStatus"
233 234
            data-qa-selector="update_resolve_status_button"
            @click="onResolveStatusUpdate"
235
          />
236
          <gl-button
237
            v-if="error.gitlabIssuePath"
238 239
            class="ml-2"
            data-qa-selector="view_issue_button"
240
            :href="error.gitlabIssuePath"
241 242 243 244 245 246 247 248 249 250 251 252 253
            variant="success"
          >
            {{ __('View issue') }}
          </gl-button>
          <form
            ref="sentryIssueForm"
            :action="projectIssuesPath"
            method="POST"
            class="d-inline-block ml-2"
          >
            <gl-form-input class="hidden" name="issue[title]" :value="issueTitle" />
            <input name="issue[description]" :value="issueDescription" type="hidden" />
            <gl-form-input
254
              :value="error.sentryId"
255 256 257 258 259
              class="hidden"
              name="issue[sentry_issue_attributes][sentry_issue_identifier]"
            />
            <gl-form-input :value="csrfToken" class="hidden" name="authenticity_token" />
            <loading-button
260
              v-if="!error.gitlabIssuePath"
261 262 263 264 265 266 267 268
              class="btn-success"
              :label="__('Create issue')"
              :loading="issueCreationInProgress"
              data-qa-selector="create_issue_button"
              @click="createIssue"
            />
          </form>
        </div>
269 270
      </div>
      <div>
271 272
        <tooltip-on-truncate :title="error.title" truncate-target="child" placement="top">
          <h2 class="text-truncate">{{ error.title }}</h2>
273
        </tooltip-on-truncate>
274
        <template v-if="error.tags">
275 276 277 278 279 280
          <gl-badge
            v-if="error.tags.level"
            :variant="errorSeverityVariant"
            class="rounded-pill mr-2"
          >
            {{ errorLevel }}
281 282 283 284 285
          </gl-badge>
          <gl-badge v-if="error.tags.logger" variant="light" class="rounded-pill"
            >{{ error.tags.logger }}
          </gl-badge>
        </template>
286
        <ul>
287
          <li v-if="error.gitlabCommit">
288
            <strong class="bold">{{ __('GitLab commit') }}:</strong>
289 290
            <gl-link :href="error.gitlabCommitPath">
              <span>{{ error.gitlabCommit.substr(0, 10) }}</span>
291 292
            </gl-link>
          </li>
293
          <li v-if="error.gitlabIssuePath">
294
            <strong class="bold">{{ __('GitLab Issue') }}:</strong>
295 296
            <gl-link :href="error.gitlabIssuePath">
              <span>{{ error.gitlabIssuePath }}</span>
297 298
            </gl-link>
          </li>
299
          <li>
300
            <strong class="bold">{{ __('Sentry event') }}:</strong>
301
            <gl-link
302
              v-track-event="trackClickErrorLinkToSentryOptions(error.externalUrl)"
303
              class="d-inline-flex align-items-center"
304
              :href="error.externalUrl"
305 306
              target="_blank"
            >
307
              <span class="text-truncate">{{ error.externalUrl }}</span>
308 309 310
              <icon name="external-link" class="ml-1 flex-shrink-0" />
            </gl-link>
          </li>
311
          <li v-if="error.firstReleaseShortVersion">
312
            <strong class="bold">{{ __('First seen') }}:</strong>
313
            {{ formatDate(error.firstSeen) }}
314
            <gl-link :href="firstReleaseLink" target="_blank">
315
              <span>{{ __('Release') }}: {{ error.firstReleaseShortVersion.substr(0, 10) }}</span>
316 317
            </gl-link>
          </li>
318
          <li v-if="error.lastReleaseShortVersion">
319
            <strong class="bold">{{ __('Last seen') }}:</strong>
320
            {{ formatDate(error.lastSeen) }}
321
            <gl-link :href="lastReleaseLink" target="_blank">
322
              <span>{{ __('Release') }}: {{ error.lastReleaseShortVersion.substr(0, 10) }}</span>
323 324 325
            </gl-link>
          </li>
          <li>
326
            <strong class="bold">{{ __('Events') }}:</strong>
327
            <span>{{ error.count }}</span>
328 329
          </li>
          <li>
330
            <strong class="bold">{{ __('Users') }}:</strong>
331
            <span>{{ error.userCount }}</span>
332 333 334 335 336 337 338
          </li>
        </ul>

        <div v-if="loadingStacktrace" class="py-3">
          <gl-loading-icon :size="3" />
        </div>

339
        <template v-else-if="showStacktrace">
340 341 342 343 344 345 346
          <h3 class="my-4">{{ __('Stack trace') }}</h3>
          <stacktrace :entries="stacktrace" />
        </template>
      </div>
    </div>
  </div>
</template>