Commit 7071f9bf authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 16bd8409
<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;
}
}
...@@ -4,7 +4,7 @@ class Admin::AbuseReportsController < Admin::ApplicationController ...@@ -4,7 +4,7 @@ class Admin::AbuseReportsController < Admin::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def index def index
@abuse_reports = AbuseReport.order(id: :desc).page(params[:page]) @abuse_reports = AbuseReport.order(id: :desc).page(params[:page])
@abuse_reports.includes(:reporter, :user) @abuse_reports = @abuse_reports.includes(:user, :reporter)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
......
- 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
---
title: Improve performance of admin/abuse_reports page
merge_request: 19630
author:
type: performance
---
title: Include exception and backtrace in API logs
merge_request: 19671
author:
type: other
# frozen_string_literal: true
class AddIndicesToAbuseReports < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :abuse_reports, :user_id
end
def down
remove_concurrent_index :abuse_reports, :user_id
end
end
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2019_11_05_094625) do ActiveRecord::Schema.define(version: 2019_11_05_140942) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm" enable_extension "pg_trgm"
...@@ -24,6 +24,7 @@ ActiveRecord::Schema.define(version: 2019_11_05_094625) do ...@@ -24,6 +24,7 @@ ActiveRecord::Schema.define(version: 2019_11_05_094625) do
t.datetime "updated_at" t.datetime "updated_at"
t.text "message_html" t.text "message_html"
t.integer "cached_markdown_version" t.integer "cached_markdown_version"
t.index ["user_id"], name: "index_abuse_reports_on_user_id"
end end
create_table "alerts_service_data", force: :cascade do |t| create_table "alerts_service_data", force: :cascade do |t|
......
...@@ -4,6 +4,24 @@ Check this document if it includes instructions for the version you are updating ...@@ -4,6 +4,24 @@ Check this document if it includes instructions for the version you are updating
These steps go together with the [general steps](updating_the_geo_nodes.md#general-update-steps) These steps go together with the [general steps](updating_the_geo_nodes.md#general-update-steps)
for updating Geo nodes. for updating Geo nodes.
## Updating to GitLab 12.2
GitLab 12.2 includes the following minor PostgreSQL updates:
- To version `9.6.14` if you run PostgreSQL 9.6.
- To version `10.9` if you run PostgreSQL 10.
This update will occur even if major PostgreSQL updates are disabled.
Before [refreshing Foreign Data Wrapper during a Geo HA upgrade](https://docs.gitlab.com/omnibus/update/README.html#run-post-deployment-migrations-and-checks),
restart the Geo tracking database:
```sh
sudo gitlab-ctl restart geo-postgresql
```
The restart avoids a version mismatch when PostgreSQL tries to load the FDW extension.
## Updating to GitLab 12.1 ## Updating to GitLab 12.1
By default, GitLab 12.1 will attempt to automatically update the By default, GitLab 12.1 will attempt to automatically update the
......
...@@ -21,6 +21,7 @@ module API ...@@ -21,6 +21,7 @@ module API
Gitlab::GrapeLogging::Loggers::ClientEnvLogger.new, Gitlab::GrapeLogging::Loggers::ClientEnvLogger.new,
Gitlab::GrapeLogging::Loggers::RouteLogger.new, Gitlab::GrapeLogging::Loggers::RouteLogger.new,
Gitlab::GrapeLogging::Loggers::UserLogger.new, Gitlab::GrapeLogging::Loggers::UserLogger.new,
Gitlab::GrapeLogging::Loggers::ExceptionLogger.new,
Gitlab::GrapeLogging::Loggers::QueueDurationLogger.new, Gitlab::GrapeLogging::Loggers::QueueDurationLogger.new,
Gitlab::GrapeLogging::Loggers::PerfLogger.new, Gitlab::GrapeLogging::Loggers::PerfLogger.new,
Gitlab::GrapeLogging::Loggers::CorrelationIdLogger.new Gitlab::GrapeLogging::Loggers::CorrelationIdLogger.new
......
...@@ -9,6 +9,7 @@ module API ...@@ -9,6 +9,7 @@ module API
GITLAB_SHARED_SECRET_HEADER = "Gitlab-Shared-Secret" GITLAB_SHARED_SECRET_HEADER = "Gitlab-Shared-Secret"
SUDO_PARAM = :sudo SUDO_PARAM = :sudo
API_USER_ENV = 'gitlab.api.user' API_USER_ENV = 'gitlab.api.user'
API_EXCEPTION_ENV = 'gitlab.api.exception'
def declared_params(options = {}) def declared_params(options = {})
options = { include_parent_namespaces: false }.merge(options) options = { include_parent_namespaces: false }.merge(options)
...@@ -387,6 +388,9 @@ module API ...@@ -387,6 +388,9 @@ module API
Gitlab::Sentry.track_acceptable_exception(exception, extra: params) Gitlab::Sentry.track_acceptable_exception(exception, extra: params)
end end
# This is used with GrapeLogging::Loggers::ExceptionLogger
env[API_EXCEPTION_ENV] = exception
# lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60 # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60
trace = exception.backtrace trace = exception.backtrace
......
# frozen_string_literal: true
module Gitlab
module GrapeLogging
module Loggers
class ExceptionLogger < ::GrapeLogging::Loggers::Base
def parameters(request, _)
# grape-logging attempts to pass the logger the exception
# (https://github.com/aserafin/grape_logging/blob/v1.7.0/lib/grape_logging/middleware/request_logger.rb#L63),
# but it appears that the rescue_all in api.rb takes
# precedence so the logger never sees it. We need to
# store and retrieve the exception from the environment.
exception = request.env[::API::Helpers::API_EXCEPTION_ENV]
return {} unless exception.is_a?(Exception)
data = {
exception: {
class: exception.class.to_s,
message: exception.message
}
}
if exception.backtrace
data[:exception][:backtrace] = Gitlab::Profiler.clean_backtrace(exception.backtrace)
end
data
end
end
end
end
end
...@@ -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 ""
......
...@@ -12,11 +12,15 @@ module QA ...@@ -12,11 +12,15 @@ module QA
fill_in 'password', with: QA::Runtime::Env.github_password fill_in 'password', with: QA::Runtime::Env.github_password
click_on 'Sign in' click_on 'Sign in'
otp = OnePassword::CLI.new.otp Support::Retrier.retry_until(exit_on_failure: true) do
otp = OnePassword::CLI.new.otp
fill_in 'otp', with: otp fill_in 'otp', with: otp
click_on 'Verify' click_on 'Verify'
!has_text?('Two-factor authentication failed', wait: 1.0)
end
click_on 'Authorize gitlab-qa' if has_button?('Authorize gitlab-qa') click_on 'Authorize gitlab-qa' if has_button?('Authorize gitlab-qa')
end end
......
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', () => {
......
require 'spec_helper'
describe Gitlab::GrapeLogging::Loggers::ExceptionLogger do
subject { described_class.new }
let(:mock_request) { OpenStruct.new(env: {}) }
describe ".parameters" do
describe 'when no exception is available' do
it 'returns an empty hash' do
expect(subject.parameters(mock_request, nil)).to eq({})
end
end
describe 'when an exception is available' do
let(:exception) { RuntimeError.new('This is a test') }
let(:mock_request) do
OpenStruct.new(
env: {
::API::Helpers::API_EXCEPTION_ENV => exception
}
)
end
let(:expected) do
{
exception: {
class: 'RuntimeError',
message: 'This is a test'
}
}
end
it 'returns the correct fields' do
expect(subject.parameters(mock_request, nil)).to eq(expected)
end
context 'with backtrace' do
before do
current_backtrace = caller
allow(exception).to receive(:backtrace).and_return(current_backtrace)
expected[:exception][:backtrace] = Gitlab::Profiler.clean_backtrace(current_backtrace)
end
it 'includes the backtrace' do
expect(subject.parameters(mock_request, nil)).to eq(expected)
end
end
end
end
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