From d5d3c03598df712550acf0c6463a61c6e7dcc19e Mon Sep 17 00:00:00 2001 From: GitLab Bot <gitlab-bot@gitlab.com> Date: Fri, 31 Jan 2020 21:08:52 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- Gemfile.lock | 4 +- .../error_tracking/components/constants.js | 6 + .../components/error_details.vue | 44 +++- .../javascripts/error_tracking/details.js | 2 - .../queries/details.query.graphql | 1 + .../error_tracking/store/actions.js | 35 ++- .../error_tracking/store/details/state.js | 1 + .../error_tracking/store/mutation_types.js | 1 + .../error_tracking/store/mutations.js | 3 + .../sentry_error_stack_trace_resolver.rb | 35 +++ .../sentry_error_collection_type.rb | 4 + .../sentry_error_stack_trace_context_type.rb | 29 +++ .../sentry_error_stack_trace_entry_type.rb | 48 ++++ .../sentry_error_stack_trace_type.rb | 22 ++ app/helpers/projects/error_tracking_helper.rb | 1 - app/views/explore/projects/_nav.html.haml | 12 +- .../19165-explore-projects-default-to-all.yml | 5 + ...6881-reverse-actions-for-status-update.yml | 5 + .../35896-graphql-error-stack-trace.yml | 5 + .../refactoring-entities-file-8.yml | 5 + config/mail_room.yml | 8 +- config/routes/explore.rb | 2 +- config/sidekiq_queues.yml | 2 + .../high_availability/README.md | 2 +- .../graphql/reference/gitlab_schema.graphql | 75 ++++++ doc/api/graphql/reference/gitlab_schema.json | 242 ++++++++++++++++++ doc/api/graphql/reference/index.md | 32 +++ lib/api/entities.rb | 69 ----- lib/api/entities/basic_ref.rb | 9 + lib/api/entities/branch.rb | 41 +++ lib/api/entities/personal_snippet.rb | 11 + lib/api/entities/project_snippet.rb | 8 + lib/api/entities/snippet.rb | 15 ++ lib/api/entities/tree_object.rb | 15 ++ lib/gitlab/error_tracking/error_event.rb | 6 +- lib/gitlab/mail_room.rb | 62 +++-- locale/gitlab.pot | 6 + qa/qa/tools/delete_subgroups.rb | 30 +-- spec/config/mail_room_spec.rb | 48 ++-- spec/features/dashboard/shortcuts_spec.rb | 2 +- spec/fixtures/config/mail_room_disabled.yml | 11 + spec/fixtures/config/mail_room_enabled.yml | 11 + .../components/error_details_spec.js | 99 ++++++- .../error_tracking/store/actions_spec.js | 64 ++--- .../sentry_error_collection_type_spec.rb | 1 + ...entry_error_stack_trace_entry_type_spec.rb | 19 ++ .../sentry_error_stack_trace_type_spec.rb | 19 ++ .../projects/error_tracking_helper_spec.rb | 5 - spec/lib/gitlab/mail_room/mail_room_spec.rb | 134 +++++----- .../sentry_errors_request_spec.rb | 91 ++++++- .../error_tracking_shared_examples.rb | 33 ++- 51 files changed, 1136 insertions(+), 304 deletions(-) create mode 100644 app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb create mode 100644 app/graphql/types/error_tracking/sentry_error_stack_trace_context_type.rb create mode 100644 app/graphql/types/error_tracking/sentry_error_stack_trace_entry_type.rb create mode 100644 app/graphql/types/error_tracking/sentry_error_stack_trace_type.rb create mode 100644 changelogs/unreleased/19165-explore-projects-default-to-all.yml create mode 100644 changelogs/unreleased/196881-reverse-actions-for-status-update.yml create mode 100644 changelogs/unreleased/35896-graphql-error-stack-trace.yml create mode 100644 changelogs/unreleased/refactoring-entities-file-8.yml create mode 100644 lib/api/entities/basic_ref.rb create mode 100644 lib/api/entities/branch.rb create mode 100644 lib/api/entities/personal_snippet.rb create mode 100644 lib/api/entities/project_snippet.rb create mode 100644 lib/api/entities/snippet.rb create mode 100644 lib/api/entities/tree_object.rb create mode 100644 spec/graphql/types/error_tracking/sentry_error_stack_trace_entry_type_spec.rb create mode 100644 spec/graphql/types/error_tracking/sentry_error_stack_trace_type_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index ee870d6d3f5..08143421c9b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -184,7 +184,7 @@ GEM unicode_utils (~> 1.4) crack (0.4.3) safe_yaml (~> 1.0.0) - crass (1.0.5) + crass (1.0.6) creole (0.5.0) css_parser (1.7.0) addressable @@ -526,7 +526,7 @@ GEM mime-types (~> 3.0) multi_xml (>= 0.5.2) httpclient (2.8.3) - i18n (1.7.0) + i18n (1.8.2) concurrent-ruby (~> 1.0) i18n_data (0.8.0) icalendar (2.4.1) diff --git a/app/assets/javascripts/error_tracking/components/constants.js b/app/assets/javascripts/error_tracking/components/constants.js index 7e3321c91bc..60b217443de 100644 --- a/app/assets/javascripts/error_tracking/components/constants.js +++ b/app/assets/javascripts/error_tracking/components/constants.js @@ -13,3 +13,9 @@ export const severityLevelVariant = { [severityLevel.INFO]: 'info', [severityLevel.DEBUG]: 'light', }; + +export const errorStatus = { + IGNORED: 'ignored', + RESOLVED: 'resolved', + UNRESOLVED: 'unresolved', +}; diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue index efc7e3c0809..98fc121d39f 100644 --- a/app/assets/javascripts/error_tracking/components/error_details.vue +++ b/app/assets/javascripts/error_tracking/components/error_details.vue @@ -11,7 +11,7 @@ import Stacktrace from './stacktrace.vue'; import TrackEventDirective from '~/vue_shared/directives/track_event'; import timeagoMixin from '~/vue_shared/mixins/timeago'; import { trackClickErrorLinkToSentryOptions } from '../utils'; -import { severityLevel, severityLevelVariant } from './constants'; +import { severityLevel, severityLevelVariant, errorStatus } from './constants'; import query from '../queries/details.query.graphql'; @@ -32,10 +32,6 @@ export default { }, mixins: [timeagoMixin], props: { - listPath: { - type: String, - required: true, - }, issueUpdatePath: { type: String, required: true, @@ -80,6 +76,7 @@ export default { result(res) { if (res.data.project?.sentryDetailedError) { this.$apollo.queries.GQLerror.stopPolling(); + this.setStatus(this.GQLerror.status); } }, }, @@ -98,6 +95,7 @@ export default { 'stacktraceData', 'updatingResolveStatus', 'updatingIgnoreStatus', + 'errorStatus', ]), ...mapGetters('details', ['stacktrace']), reported() { @@ -153,20 +151,40 @@ export default { 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'); + }, }, mounted() { this.startPollingDetails(this.issueDetailsPath); this.startPollingStacktrace(this.issueStackTracePath); }, methods: { - ...mapActions('details', ['startPollingDetails', 'startPollingStacktrace', 'updateStatus']), + ...mapActions('details', [ + 'startPollingDetails', + 'startPollingStacktrace', + 'updateStatus', + 'setStatus', + 'updateResolveStatus', + 'updateIgnoreStatus', + ]), trackClickErrorLinkToSentryOptions, createIssue() { this.issueCreationInProgress = true; this.$refs.sentryIssueForm.submit(); }, - updateIssueStatus(status) { - this.updateStatus({ endpoint: this.issueUpdatePath, redirectUrl: this.listPath, status }); + 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; + this.updateResolveStatus({ endpoint: this.issueUpdatePath, status }); }, formatDate(date) { return `${this.timeFormatted(date)} (${dateFormat(date, 'UTC:yyyy-mm-dd h:MM:ssTT Z')})`; @@ -185,15 +203,17 @@ export default { <span v-if="!loadingStacktrace && stacktrace" v-html="reported"></span> <div class="d-inline-flex"> <loading-button - :label="__('Ignore')" + :label="ignoreBtnLabel" :loading="updatingIgnoreStatus" - @click="updateIssueStatus('ignored')" + data-qa-selector="update_ignore_status_button" + @click="onIgnoreStatusUpdate" /> <loading-button class="btn-outline-info ml-2" - :label="__('Resolve')" + :label="resolveBtnLabel" :loading="updatingResolveStatus" - @click="updateIssueStatus('resolved')" + data-qa-selector="update_resolve_status_button" + @click="onResolveStatusUpdate" /> <gl-button v-if="error.gitlab_issue" diff --git a/app/assets/javascripts/error_tracking/details.js b/app/assets/javascripts/error_tracking/details.js index c18298dec4f..a5a7ddc907b 100644 --- a/app/assets/javascripts/error_tracking/details.js +++ b/app/assets/javascripts/error_tracking/details.js @@ -25,7 +25,6 @@ export default () => { const { issueId, projectPath, - listPath, issueUpdatePath, issueDetailsPath, issueStackTracePath, @@ -36,7 +35,6 @@ export default () => { props: { issueId, projectPath, - listPath, issueUpdatePath, issueDetailsPath, issueStackTracePath, diff --git a/app/assets/javascripts/error_tracking/queries/details.query.graphql b/app/assets/javascripts/error_tracking/queries/details.query.graphql index 625ce3030d9..488a3ecc3ab 100644 --- a/app/assets/javascripts/error_tracking/queries/details.query.graphql +++ b/app/assets/javascripts/error_tracking/queries/details.query.graphql @@ -6,6 +6,7 @@ query errorDetails($fullPath: ID!, $errorId: ID!) { title userCount count + status firstSeen lastSeen message diff --git a/app/assets/javascripts/error_tracking/store/actions.js b/app/assets/javascripts/error_tracking/store/actions.js index bb8b039b5df..49fa5f3cec5 100644 --- a/app/assets/javascripts/error_tracking/store/actions.js +++ b/app/assets/javascripts/error_tracking/store/actions.js @@ -4,16 +4,33 @@ import createFlash from '~/flash'; import { visitUrl } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; -export function updateStatus({ commit }, { endpoint, redirectUrl, status }) { - const type = - status === 'resolved' ? types.SET_UPDATING_RESOLVE_STATUS : types.SET_UPDATING_IGNORE_STATUS; - commit(type, true); +export const setStatus = ({ commit }, status) => { + commit(types.SET_ERROR_STATUS, status.toLowerCase()); +}; - return service +export const updateStatus = ({ commit }, { endpoint, redirectUrl, status }) => + service .updateErrorStatus(endpoint, status) - .then(() => visitUrl(redirectUrl)) - .catch(() => createFlash(__('Failed to update issue status'))) - .finally(() => commit(type, false)); -} + .then(() => { + if (redirectUrl) visitUrl(redirectUrl); + commit(types.SET_ERROR_STATUS, status); + }) + .catch(() => createFlash(__('Failed to update issue status'))); + +export const updateResolveStatus = ({ commit, dispatch }, params) => { + commit(types.SET_UPDATING_RESOLVE_STATUS, true); + + return dispatch('updateStatus', params).finally(() => { + commit(types.SET_UPDATING_RESOLVE_STATUS, false); + }); +}; + +export const updateIgnoreStatus = ({ commit, dispatch }, params) => { + commit(types.SET_UPDATING_IGNORE_STATUS, true); + + return dispatch('updateStatus', params).finally(() => { + commit(types.SET_UPDATING_IGNORE_STATUS, false); + }); +}; export default () => {}; diff --git a/app/assets/javascripts/error_tracking/store/details/state.js b/app/assets/javascripts/error_tracking/store/details/state.js index 52b0297607d..f53cbe29c67 100644 --- a/app/assets/javascripts/error_tracking/store/details/state.js +++ b/app/assets/javascripts/error_tracking/store/details/state.js @@ -5,4 +5,5 @@ export default () => ({ loadingStacktrace: true, updatingResolveStatus: false, updatingIgnoreStatus: false, + errorStatus: '', }); diff --git a/app/assets/javascripts/error_tracking/store/mutation_types.js b/app/assets/javascripts/error_tracking/store/mutation_types.js index 30aebacbedd..a7ac6ab2e60 100644 --- a/app/assets/javascripts/error_tracking/store/mutation_types.js +++ b/app/assets/javascripts/error_tracking/store/mutation_types.js @@ -1,2 +1,3 @@ export const SET_UPDATING_RESOLVE_STATUS = 'SET_UPDATING_RESOLVE_STATUS'; export const SET_UPDATING_IGNORE_STATUS = 'SET_UPDATING_IGNORE_STATUS'; +export const SET_ERROR_STATUS = 'SET_ERROR_STATUS'; diff --git a/app/assets/javascripts/error_tracking/store/mutations.js b/app/assets/javascripts/error_tracking/store/mutations.js index c7a7e46df40..8f2d9bcbe85 100644 --- a/app/assets/javascripts/error_tracking/store/mutations.js +++ b/app/assets/javascripts/error_tracking/store/mutations.js @@ -7,4 +7,7 @@ export default { [types.SET_UPDATING_RESOLVE_STATUS](state, updating) { state.updatingResolveStatus = updating; }, + [types.SET_ERROR_STATUS](state, status) { + state.errorStatus = status; + }, }; diff --git a/app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb b/app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb new file mode 100644 index 00000000000..f5356660569 --- /dev/null +++ b/app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Resolvers + module ErrorTracking + class SentryErrorStackTraceResolver < BaseResolver + argument :id, GraphQL::ID_TYPE, + required: true, + description: 'ID of the Sentry issue' + + def resolve(**args) + issue_id = GlobalID.parse(args[:id]).model_id + + # Get data from Sentry + response = ::ErrorTracking::IssueLatestEventService.new( + project, + current_user, + { issue_id: issue_id } + ).execute + + event = response[:latest_event] + event.gitlab_project = project if event + + event + end + + private + + def project + return object.gitlab_project if object.respond_to?(:gitlab_project) + + object + end + end + end +end diff --git a/app/graphql/types/error_tracking/sentry_error_collection_type.rb b/app/graphql/types/error_tracking/sentry_error_collection_type.rb index 2e1b75ac84c..121146133cb 100644 --- a/app/graphql/types/error_tracking/sentry_error_collection_type.rb +++ b/app/graphql/types/error_tracking/sentry_error_collection_type.rb @@ -28,6 +28,10 @@ module Types null: true, description: 'Detailed version of a Sentry error on the project', resolver: Resolvers::ErrorTracking::SentryDetailedErrorResolver + field :error_stack_trace, Types::ErrorTracking::SentryErrorStackTraceType, + null: true, + description: 'Stack Trace of Sentry Error', + resolver: Resolvers::ErrorTracking::SentryErrorStackTraceResolver field :external_url, GraphQL::STRING_TYPE, null: true, diff --git a/app/graphql/types/error_tracking/sentry_error_stack_trace_context_type.rb b/app/graphql/types/error_tracking/sentry_error_stack_trace_context_type.rb new file mode 100644 index 00000000000..e6d02c948d5 --- /dev/null +++ b/app/graphql/types/error_tracking/sentry_error_stack_trace_context_type.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Types + module ErrorTracking + # rubocop: disable Graphql/AuthorizeTypes + class SentryErrorStackTraceContextType < ::Types::BaseObject + graphql_name 'SentryErrorStackTraceContext' + description 'An object context for a Sentry error stack trace' + + field :line, + GraphQL::INT_TYPE, + null: false, + description: 'Line number of the context' + field :code, + GraphQL::STRING_TYPE, + null: false, + description: 'Code number of the context' + + def line + object[0] + end + + def code + object[1] + end + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/error_tracking/sentry_error_stack_trace_entry_type.rb b/app/graphql/types/error_tracking/sentry_error_stack_trace_entry_type.rb new file mode 100644 index 00000000000..0747e41e9fb --- /dev/null +++ b/app/graphql/types/error_tracking/sentry_error_stack_trace_entry_type.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Types + module ErrorTracking + # rubocop: disable Graphql/AuthorizeTypes + class SentryErrorStackTraceEntryType < ::Types::BaseObject + graphql_name 'SentryErrorStackTraceEntry' + description 'An object containing a stack trace entry for a Sentry error.' + + field :function, GraphQL::STRING_TYPE, + null: true, + description: 'Function in which the Sentry error occurred' + field :col, GraphQL::STRING_TYPE, + null: true, + description: 'Function in which the Sentry error occurred' + field :line, GraphQL::STRING_TYPE, + null: true, + description: 'Function in which the Sentry error occurred' + field :file_name, GraphQL::STRING_TYPE, + null: true, + description: 'File in which the Sentry error occurred' + field :trace_context, [Types::ErrorTracking::SentryErrorStackTraceContextType], + null: true, + description: 'Context of the Sentry error' + + def function + object['function'] + end + + def col + object['colNo'] + end + + def line + object['lineNo'] + end + + def file_name + object['filename'] + end + + def trace_context + object['context'] + end + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/error_tracking/sentry_error_stack_trace_type.rb b/app/graphql/types/error_tracking/sentry_error_stack_trace_type.rb new file mode 100644 index 00000000000..0e6105d1ff2 --- /dev/null +++ b/app/graphql/types/error_tracking/sentry_error_stack_trace_type.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Types + module ErrorTracking + class SentryErrorStackTraceType < ::Types::BaseObject + graphql_name 'SentryErrorStackTrace' + description 'An object containing a stack trace entry for a Sentry error.' + + authorize :read_sentry_issue + + field :issue_id, GraphQL::STRING_TYPE, + null: false, + description: 'ID of the Sentry error' + field :date_received, GraphQL::STRING_TYPE, + null: false, + description: 'Time the stack trace was received by Sentry' + field :stack_trace_entries, [Types::ErrorTracking::SentryErrorStackTraceEntryType], + null: false, + description: 'Stack trace entries for the Sentry error' + end + end +end diff --git a/app/helpers/projects/error_tracking_helper.rb b/app/helpers/projects/error_tracking_helper.rb index ed5c7640ec1..91fc18f4312 100644 --- a/app/helpers/projects/error_tracking_helper.rb +++ b/app/helpers/projects/error_tracking_helper.rb @@ -22,7 +22,6 @@ module Projects::ErrorTrackingHelper { 'issue-id' => issue_id, 'project-path' => project.full_path, - 'list-path' => project_error_tracking_index_path(project), '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), diff --git a/app/views/explore/projects/_nav.html.haml b/app/views/explore/projects/_nav.html.haml index bf65c19b720..65b7d055843 100644 --- a/app/views/explore/projects/_nav.html.haml +++ b/app/views/explore/projects/_nav.html.haml @@ -1,14 +1,14 @@ .top-area %ul.nav-links.nav.nav-tabs - = nav_link(page: [trending_explore_projects_path, explore_root_path]) do - = link_to trending_explore_projects_path do - = _('Trending') + = nav_link(page: [explore_projects_path, explore_root_path]) do + = link_to explore_projects_path do + = _('All') = nav_link(page: starred_explore_projects_path) do = link_to starred_explore_projects_path do = _('Most stars') - = nav_link(page: explore_projects_path) do - = link_to explore_projects_path do - = _('All') + = nav_link(page: trending_explore_projects_path) do + = link_to trending_explore_projects_path do + = _('Trending') .nav-controls - unless current_user diff --git a/changelogs/unreleased/19165-explore-projects-default-to-all.yml b/changelogs/unreleased/19165-explore-projects-default-to-all.yml new file mode 100644 index 00000000000..ea26a3939dd --- /dev/null +++ b/changelogs/unreleased/19165-explore-projects-default-to-all.yml @@ -0,0 +1,5 @@ +--- +title: Make Explore Projects default to All +merge_request: 23811 +author: +type: changed diff --git a/changelogs/unreleased/196881-reverse-actions-for-status-update.yml b/changelogs/unreleased/196881-reverse-actions-for-status-update.yml new file mode 100644 index 00000000000..f82bab07e14 --- /dev/null +++ b/changelogs/unreleased/196881-reverse-actions-for-status-update.yml @@ -0,0 +1,5 @@ +--- +title: Reverse actions for resolve/ignore Sentry issue +merge_request: 23516 +author: +type: added diff --git a/changelogs/unreleased/35896-graphql-error-stack-trace.yml b/changelogs/unreleased/35896-graphql-error-stack-trace.yml new file mode 100644 index 00000000000..487aa603d53 --- /dev/null +++ b/changelogs/unreleased/35896-graphql-error-stack-trace.yml @@ -0,0 +1,5 @@ +--- +title: Add Sentry error stack trace to GraphQL API +merge_request: 23750 +author: +type: added diff --git a/changelogs/unreleased/refactoring-entities-file-8.yml b/changelogs/unreleased/refactoring-entities-file-8.yml new file mode 100644 index 00000000000..14f377a2dcf --- /dev/null +++ b/changelogs/unreleased/refactoring-entities-file-8.yml @@ -0,0 +1,5 @@ +--- +title: Separate snippet entities into own class files +merge_request: 24183 +author: Rajendra Kadam +type: added diff --git a/config/mail_room.yml b/config/mail_room.yml index 75024c2b2e1..da37ef60587 100644 --- a/config/mail_room.yml +++ b/config/mail_room.yml @@ -1,9 +1,7 @@ :mailboxes: <% require_relative "../lib/gitlab/mail_room" unless defined?(Gitlab::MailRoom) - config = Gitlab::MailRoom.config - - if Gitlab::MailRoom.enabled? + Gitlab::MailRoom.enabled_configs.each do |config| %> - :host: <%= config[:host].to_json %> @@ -24,8 +22,8 @@ :delivery_options: :redis_url: <%= config[:redis_url].to_json %> :namespace: <%= Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE %> - :queue: email_receiver - :worker: EmailReceiverWorker + :queue: <%= config[:queue] %> + :worker: <%= config[:worker] %> <% if config[:sentinels] %> :sentinels: <% config[:sentinels].each do |sentinel| %> diff --git a/config/routes/explore.rb b/config/routes/explore.rb index 42ec5e8abec..59b53bdcf42 100644 --- a/config/routes/explore.rb +++ b/config/routes/explore.rb @@ -8,7 +8,7 @@ namespace :explore do resources :groups, only: [:index] resources :snippets, only: [:index] - root to: 'projects#trending' + root to: 'projects#index' end # Compatibility with old routing diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 8974b646cd9..1e1ed52650b 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -224,6 +224,8 @@ - 2 - - self_monitoring_project_delete - 2 +- - service_desk_email_receiver + - 1 - - system_hook_push - 1 - - todos_destroyer diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md index 09747595fc0..2c2fc075dbe 100644 --- a/doc/administration/high_availability/README.md +++ b/doc/administration/high_availability/README.md @@ -200,7 +200,7 @@ with the added complexity of many more nodes to configure, manage, and monitor. ![Fully Distributed architecture diagram](img/fully-distributed.png) -## Reference Architecture Examples +## Reference Architecture Recommendations The Support and Quality teams build, performance test, and validate Reference Architectures that support large numbers of users. The specifications below are diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index b654e8bbd8b..f0ad0b8184d 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -6298,6 +6298,16 @@ type SentryErrorCollection { id: ID! ): SentryDetailedError + """ + Stack Trace of Sentry Error + """ + errorStackTrace( + """ + ID of the Sentry issue + """ + id: ID! + ): SentryErrorStackTrace + """ Collection of Sentry Errors """ @@ -6386,6 +6396,71 @@ type SentryErrorFrequency { time: Time! } +""" +An object containing a stack trace entry for a Sentry error. +""" +type SentryErrorStackTrace { + """ + Time the stack trace was received by Sentry + """ + dateReceived: String! + + """ + ID of the Sentry error + """ + issueId: String! + + """ + Stack trace entries for the Sentry error + """ + stackTraceEntries: [SentryErrorStackTraceEntry!]! +} + +""" +An object context for a Sentry error stack trace +""" +type SentryErrorStackTraceContext { + """ + Code number of the context + """ + code: String! + + """ + Line number of the context + """ + line: Int! +} + +""" +An object containing a stack trace entry for a Sentry error. +""" +type SentryErrorStackTraceEntry { + """ + Function in which the Sentry error occurred + """ + col: String + + """ + File in which the Sentry error occurred + """ + fileName: String + + """ + Function in which the Sentry error occurred + """ + function: String + + """ + Function in which the Sentry error occurred + """ + line: String + + """ + Context of the Sentry error + """ + traceContext: [SentryErrorStackTraceContext!] +} + """ State of a Sentry error """ diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index a49a9c65f4e..33958dde42c 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -17454,6 +17454,33 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "errorStackTrace", + "description": "Stack Trace of Sentry Error", + "args": [ + { + "name": "id", + "description": "ID of the Sentry issue", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "SentryErrorStackTrace", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "errors", "description": "Collection of Sentry Errors", @@ -17984,6 +18011,221 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "SentryErrorStackTrace", + "description": "An object containing a stack trace entry for a Sentry error.", + "fields": [ + { + "name": "dateReceived", + "description": "Time the stack trace was received by Sentry", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "issueId", + "description": "ID of the Sentry error", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "stackTraceEntries", + "description": "Stack trace entries for the Sentry error", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SentryErrorStackTraceEntry", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SentryErrorStackTraceEntry", + "description": "An object containing a stack trace entry for a Sentry error.", + "fields": [ + { + "name": "col", + "description": "Function in which the Sentry error occurred", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fileName", + "description": "File in which the Sentry error occurred", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "function", + "description": "Function in which the Sentry error occurred", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "line", + "description": "Function in which the Sentry error occurred", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "traceContext", + "description": "Context of the Sentry error", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SentryErrorStackTraceContext", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SentryErrorStackTraceContext", + "description": "An object context for a Sentry error stack trace", + "fields": [ + { + "name": "code", + "description": "Code number of the context", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "line", + "description": "Line number of the context", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "Metadata", diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 5325c5ff335..11e48d69165 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -983,6 +983,7 @@ An object containing a collection of Sentry errors, and a detailed error. | Name | Type | Description | | --- | ---- | ---------- | | `detailedError` | SentryDetailedError | Detailed version of a Sentry error on the project | +| `errorStackTrace` | SentryErrorStackTrace | Stack Trace of Sentry Error | | `errors` | SentryErrorConnection | Collection of Sentry Errors | | `externalUrl` | String | External URL for Sentry | @@ -993,6 +994,37 @@ An object containing a collection of Sentry errors, and a detailed error. | `count` | Int! | Count of errors received since the previously recorded time | | `time` | Time! | Time the error frequency stats were recorded | +## SentryErrorStackTrace + +An object containing a stack trace entry for a Sentry error. + +| Name | Type | Description | +| --- | ---- | ---------- | +| `dateReceived` | String! | Time the stack trace was received by Sentry | +| `issueId` | String! | ID of the Sentry error | +| `stackTraceEntries` | SentryErrorStackTraceEntry! => Array | Stack trace entries for the Sentry error | + +## SentryErrorStackTraceContext + +An object context for a Sentry error stack trace + +| Name | Type | Description | +| --- | ---- | ---------- | +| `code` | String! | Code number of the context | +| `line` | Int! | Line number of the context | + +## SentryErrorStackTraceEntry + +An object containing a stack trace entry for a Sentry error. + +| Name | Type | Description | +| --- | ---- | ---------- | +| `col` | String | Function in which the Sentry error occurred | +| `fileName` | String | File in which the Sentry error occurred | +| `function` | String | Function in which the Sentry error occurred | +| `line` | String | Function in which the Sentry error occurred | +| `traceContext` | SentryErrorStackTraceContext! => Array | Context of the Sentry error | + ## SentryErrorTags State of a Sentry error diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 451d7c9edca..88ca0906283 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -128,75 +128,6 @@ module API end end - class BasicRef < Grape::Entity - expose :type, :name - end - - class Branch < Grape::Entity - expose :name - - expose :commit, using: Entities::Commit do |repo_branch, options| - options[:project].repository.commit(repo_branch.dereferenced_target) - end - - expose :merged do |repo_branch, options| - if options[:merged_branch_names] - options[:merged_branch_names].include?(repo_branch.name) - else - options[:project].repository.merged_to_root_ref?(repo_branch) - end - end - - expose :protected do |repo_branch, options| - ::ProtectedBranch.protected?(options[:project], repo_branch.name) - end - - expose :developers_can_push do |repo_branch, options| - ::ProtectedBranch.developers_can?(:push, repo_branch.name, protected_refs: options[:project].protected_branches) - end - - expose :developers_can_merge do |repo_branch, options| - ::ProtectedBranch.developers_can?(:merge, repo_branch.name, protected_refs: options[:project].protected_branches) - end - - expose :can_push do |repo_branch, options| - Gitlab::UserAccess.new(options[:current_user], project: options[:project]).can_push_to_branch?(repo_branch.name) - end - - expose :default do |repo_branch, options| - options[:project].default_branch == repo_branch.name - end - end - - class TreeObject < Grape::Entity - expose :id, :name, :type, :path - - expose :mode do |obj, options| - filemode = obj.mode - filemode = "0" + filemode if filemode.length < 6 - filemode - end - end - - class Snippet < Grape::Entity - expose :id, :title, :file_name, :description, :visibility - expose :author, using: Entities::UserBasic - expose :updated_at, :created_at - expose :project_id - expose :web_url do |snippet| - Gitlab::UrlBuilder.build(snippet) - end - end - - class ProjectSnippet < Snippet - end - - class PersonalSnippet < Snippet - expose :raw_url do |snippet| - Gitlab::UrlBuilder.build(snippet, raw: true) - end - end - class IssuableEntity < Grape::Entity expose :id, :iid expose(:project_id) { |entity| entity&.project.try(:id) } diff --git a/lib/api/entities/basic_ref.rb b/lib/api/entities/basic_ref.rb new file mode 100644 index 00000000000..79c15075d99 --- /dev/null +++ b/lib/api/entities/basic_ref.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class BasicRef < Grape::Entity + expose :type, :name + end + end +end diff --git a/lib/api/entities/branch.rb b/lib/api/entities/branch.rb new file mode 100644 index 00000000000..1d5017ac702 --- /dev/null +++ b/lib/api/entities/branch.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module API + module Entities + class Branch < Grape::Entity + expose :name + + expose :commit, using: Entities::Commit do |repo_branch, options| + options[:project].repository.commit(repo_branch.dereferenced_target) + end + + expose :merged do |repo_branch, options| + if options[:merged_branch_names] + options[:merged_branch_names].include?(repo_branch.name) + else + options[:project].repository.merged_to_root_ref?(repo_branch) + end + end + + expose :protected do |repo_branch, options| + ::ProtectedBranch.protected?(options[:project], repo_branch.name) + end + + expose :developers_can_push do |repo_branch, options| + ::ProtectedBranch.developers_can?(:push, repo_branch.name, protected_refs: options[:project].protected_branches) + end + + expose :developers_can_merge do |repo_branch, options| + ::ProtectedBranch.developers_can?(:merge, repo_branch.name, protected_refs: options[:project].protected_branches) + end + + expose :can_push do |repo_branch, options| + Gitlab::UserAccess.new(options[:current_user], project: options[:project]).can_push_to_branch?(repo_branch.name) + end + + expose :default do |repo_branch, options| + options[:project].default_branch == repo_branch.name + end + end + end +end diff --git a/lib/api/entities/personal_snippet.rb b/lib/api/entities/personal_snippet.rb new file mode 100644 index 00000000000..eb0266e61e6 --- /dev/null +++ b/lib/api/entities/personal_snippet.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class PersonalSnippet < Snippet + expose :raw_url do |snippet| + Gitlab::UrlBuilder.build(snippet, raw: true) + end + end + end +end diff --git a/lib/api/entities/project_snippet.rb b/lib/api/entities/project_snippet.rb new file mode 100644 index 00000000000..8ed87e51375 --- /dev/null +++ b/lib/api/entities/project_snippet.rb @@ -0,0 +1,8 @@ +# frozen_String_literal: true + +module API + module Entities + class ProjectSnippet < Entities::Snippet + end + end +end diff --git a/lib/api/entities/snippet.rb b/lib/api/entities/snippet.rb new file mode 100644 index 00000000000..d92f7b79c28 --- /dev/null +++ b/lib/api/entities/snippet.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module API + module Entities + class Snippet < Grape::Entity + expose :id, :title, :file_name, :description, :visibility + expose :author, using: Entities::UserBasic + expose :updated_at, :created_at + expose :project_id + expose :web_url do |snippet| + Gitlab::UrlBuilder.build(snippet) + end + end + end +end diff --git a/lib/api/entities/tree_object.rb b/lib/api/entities/tree_object.rb new file mode 100644 index 00000000000..e4e840ebe43 --- /dev/null +++ b/lib/api/entities/tree_object.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module API + module Entities + class TreeObject < Grape::Entity + expose :id, :name, :type, :path + + expose :mode do |obj, options| + filemode = obj.mode + filemode = "0" + filemode if filemode.length < 6 + filemode + end + end + end +end diff --git a/lib/gitlab/error_tracking/error_event.rb b/lib/gitlab/error_tracking/error_event.rb index c6e0d82f868..015d2c0ead0 100644 --- a/lib/gitlab/error_tracking/error_event.rb +++ b/lib/gitlab/error_tracking/error_event.rb @@ -5,7 +5,11 @@ module Gitlab class ErrorEvent include ActiveModel::Model - attr_accessor :issue_id, :date_received, :stack_trace_entries + attr_accessor :issue_id, :date_received, :stack_trace_entries, :gitlab_project + + def self.declarative_policy_class + 'ErrorTracking::BasePolicy' + end end end end diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb index f7699ef1718..bd69843adf1 100644 --- a/lib/gitlab/mail_room.rb +++ b/lib/gitlab/mail_room.rb @@ -2,6 +2,7 @@ require 'yaml' require 'json' +require 'pathname' require_relative 'redis/queues' unless defined?(Gitlab::Redis::Queues) # This service is run independently of the main Rails process, @@ -21,39 +22,60 @@ module Gitlab log_path: RAILS_ROOT_DIR.join('log', 'mail_room_json.log') }.freeze + # Email specific configuration which is merged with configuration + # fetched from YML config file. + ADDRESS_SPECIFIC_CONFIG = { + incoming_email: { + queue: 'email_receiver', + worker: 'EmailReceiverWorker' + }, + service_desk_email: { + queue: 'service_desk_email_receiver', + worker: 'ServiceDeskEmailReceiverWorker' + } + }.freeze + class << self - def enabled? - config[:enabled] && config[:address] + def enabled_configs + @enabled_configs ||= configs.select { |config| enabled?(config) } end - def config - @config ||= fetch_config - end + private - def reset_config! - @config = nil + def enabled?(config) + config[:enabled] && !config[:address].to_s.empty? end - private + def configs + ADDRESS_SPECIFIC_CONFIG.keys.map { |key| fetch_config(key) } + end - def fetch_config + def fetch_config(config_key) return {} unless File.exist?(config_file) - config = load_from_yaml || {} - config = DEFAULT_CONFIG.merge(config) do |_key, oldval, newval| + config = merged_configs(config_key) + config.merge!(redis_config) if enabled?(config) + config[:log_path] = File.expand_path(config[:log_path], RAILS_ROOT_DIR) + + config + end + + def merged_configs(config_key) + yml_config = load_yaml.fetch(config_key, {}) + specific_config = ADDRESS_SPECIFIC_CONFIG.fetch(config_key, {}) + DEFAULT_CONFIG.merge(specific_config, yml_config) do |_key, oldval, newval| newval.nil? ? oldval : newval end + end - if config[:enabled] && config[:address] - gitlab_redis_queues = Gitlab::Redis::Queues.new(rails_env) - config[:redis_url] = gitlab_redis_queues.url + def redis_config + gitlab_redis_queues = Gitlab::Redis::Queues.new(rails_env) + config = { redis_url: gitlab_redis_queues.url } - if gitlab_redis_queues.sentinels? - config[:sentinels] = gitlab_redis_queues.sentinels - end + if gitlab_redis_queues.sentinels? + config[:sentinels] = gitlab_redis_queues.sentinels end - config[:log_path] = File.expand_path(config[:log_path], RAILS_ROOT_DIR) config end @@ -65,8 +87,8 @@ module Gitlab ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] || File.expand_path('../../config/gitlab.yml', __dir__) end - def load_from_yaml - YAML.load_file(config_file)[rails_env].deep_symbolize_keys[:incoming_email] + def load_yaml + @yaml ||= YAML.load_file(config_file)[rails_env].deep_symbolize_keys end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1c1fc8d16ff..473490ba7bb 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -20259,6 +20259,9 @@ msgstr "" msgid "Undo" msgstr "" +msgid "Undo ignore" +msgstr "" + msgid "Unfortunately, your email message to GitLab could not be processed." msgstr "" @@ -20310,6 +20313,9 @@ msgstr "" msgid "Unmarks this %{noun} as Work In Progress." msgstr "" +msgid "Unresolve" +msgstr "" + msgid "Unresolve discussion" msgstr "" diff --git a/qa/qa/tools/delete_subgroups.rb b/qa/qa/tools/delete_subgroups.rb index 3f752adbe6f..a05ec735632 100644 --- a/qa/qa/tools/delete_subgroups.rb +++ b/qa/qa/tools/delete_subgroups.rb @@ -26,30 +26,19 @@ module QA group_id = fetch_group_id sub_groups_head_response = head Runtime::API::Request.new(@api_client, "/groups/#{group_id}/subgroups", per_page: "100").url - total_sub_groups = sub_groups_head_response.headers[:x_total] total_sub_group_pages = sub_groups_head_response.headers[:x_total_pages] - STDOUT.puts "total_sub_groups: #{total_sub_groups}" - STDOUT.puts "total_sub_group_pages: #{total_sub_group_pages}" + sub_group_ids = fetch_subgroup_ids(group_id, total_sub_group_pages) + STDOUT.puts "Number of Sub Groups not already marked for deletion: #{sub_group_ids.length}" - total_sub_group_pages.to_i.times do |page_no| - # Fetch all subgroups for the top level group - sub_groups_response = get Runtime::API::Request.new(@api_client, "/groups/#{group_id}/subgroups", per_page: "100").url - - sub_group_ids = JSON.parse(sub_groups_response.body).map { |subgroup| subgroup["id"] } - - if sub_group_ids.any? - STDOUT.puts "\n==== Current Page: #{page_no + 1} ====\n" - - delete_subgroups(sub_group_ids) - end - end + delete_subgroups(sub_group_ids) unless sub_group_ids.empty? STDOUT.puts "\nDone" end private def delete_subgroups(sub_group_ids) + STDOUT.puts "Deleting #{sub_group_ids.length} subgroups..." sub_group_ids.each do |subgroup_id| delete_response = delete Runtime::API::Request.new(@api_client, "/groups/#{subgroup_id}").url dot_or_f = delete_response.code == 202 ? "\e[32m.\e[0m" : "\e[31mF\e[0m" @@ -61,6 +50,17 @@ module QA group_search_response = get Runtime::API::Request.new(@api_client, "/groups", search: ENV['GROUP_NAME_OR_PATH'] || 'gitlab-qa-sandbox-group').url JSON.parse(group_search_response.body).first["id"] end + + def fetch_subgroup_ids(group_id, group_pages) + sub_groups_ids = [] + + group_pages.to_i.times do |page_no| + sub_groups_response = get Runtime::API::Request.new(@api_client, "/groups/#{group_id}/subgroups", page: (page_no + 1).to_s, per_page: "100").url + sub_groups_ids.concat(JSON.parse(sub_groups_response.body).reject { |subgroup| !subgroup["marked_for_deletion_on"].nil? }.map { |subgroup| subgroup["id"] }) + end + + sub_groups_ids.uniq + end end end end diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb index 94b29b89f24..fcef4e7a9b0 100644 --- a/spec/config/mail_room_spec.rb +++ b/spec/config/mail_room_spec.rb @@ -39,39 +39,31 @@ describe 'mail_room.yml' do end end - context 'when incoming email is enabled' do + context 'when both incoming email and service desk email are enabled' do let(:gitlab_config_path) { 'spec/fixtures/config/mail_room_enabled.yml' } let(:queues_config_path) { 'spec/fixtures/config/redis_queues_new_format_host.yml' } - let(:gitlab_redis_queues) { Gitlab::Redis::Queues.new(Rails.env) } it 'contains the intended configuration' do - expect(configuration[:mailboxes].length).to eq(1) - mailbox = configuration[:mailboxes].first - - expect(mailbox[:host]).to eq('imap.gmail.com') - expect(mailbox[:port]).to eq(993) - expect(mailbox[:ssl]).to eq(true) - expect(mailbox[:start_tls]).to eq(false) - expect(mailbox[:email]).to eq('gitlab-incoming@gmail.com') - expect(mailbox[:password]).to eq('[REDACTED]') - expect(mailbox[:name]).to eq('inbox') - expect(mailbox[:idle_timeout]).to eq(60) - - redis_url = gitlab_redis_queues.url - sentinels = gitlab_redis_queues.sentinels - - expect(mailbox[:delivery_options][:redis_url]).to be_present - expect(mailbox[:delivery_options][:redis_url]).to eq(redis_url) - - expect(mailbox[:delivery_options][:sentinels]).to be_present - expect(mailbox[:delivery_options][:sentinels]).to eq(sentinels) - - expect(mailbox[:arbitration_options][:redis_url]).to be_present - expect(mailbox[:arbitration_options][:redis_url]).to eq(redis_url) - - expect(mailbox[:arbitration_options][:sentinels]).to be_present - expect(mailbox[:arbitration_options][:sentinels]).to eq(sentinels) + expected_mailbox = { + host: 'imap.gmail.com', + port: 993, + ssl: true, + start_tls: false, + email: 'gitlab-incoming@gmail.com', + password: '[REDACTED]', + name: 'inbox', + idle_timeout: 60 + } + expected_options = { + redis_url: gitlab_redis_queues.url, + sentinels: gitlab_redis_queues.sentinels + } + + expect(configuration[:mailboxes].length).to eq(2) + expect(configuration[:mailboxes]).to all(include(expected_mailbox)) + expect(configuration[:mailboxes].map { |m| m[:delivery_options] }).to all(include(expected_options)) + expect(configuration[:mailboxes].map { |m| m[:arbitration_options] }).to all(include(expected_options)) end end diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb index 3a47475da2b..cf74b2cc8ce 100644 --- a/spec/features/dashboard/shortcuts_spec.rb +++ b/spec/features/dashboard/shortcuts_spec.rb @@ -51,7 +51,7 @@ describe 'Dashboard shortcuts', :js do find('body').send_keys([:shift, 'P']) find('.nothing-here-block') - expect(page).to have_content('Explore public groups to find projects to contribute to.') + expect(page).to have_content("This user doesn't have any personal projects") end end diff --git a/spec/fixtures/config/mail_room_disabled.yml b/spec/fixtures/config/mail_room_disabled.yml index 97f8cff051f..538f2a35f81 100644 --- a/spec/fixtures/config/mail_room_disabled.yml +++ b/spec/fixtures/config/mail_room_disabled.yml @@ -9,3 +9,14 @@ test: ssl: true start_tls: false mailbox: "inbox" + + service_desk_email: + enabled: false + address: "gitlab-incoming+%{key}@gmail.com" + user: "gitlab-incoming@gmail.com" + password: "[REDACTED]" + host: "imap.gmail.com" + port: 993 + ssl: true + start_tls: false + mailbox: "inbox" diff --git a/spec/fixtures/config/mail_room_enabled.yml b/spec/fixtures/config/mail_room_enabled.yml index 9c94649244d..e1f4c2f44de 100644 --- a/spec/fixtures/config/mail_room_enabled.yml +++ b/spec/fixtures/config/mail_room_enabled.yml @@ -9,3 +9,14 @@ test: ssl: true start_tls: false mailbox: "inbox" + + service_desk_email: + enabled: true + address: "gitlab-incoming+%{key}@gmail.com" + user: "gitlab-incoming@gmail.com" + password: "[REDACTED]" + host: "imap.gmail.com" + port: 993 + ssl: true + start_tls: false + mailbox: "inbox" diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js index f0578b52922..1e1d20800da 100644 --- a/spec/frontend/error_tracking/components/error_details_spec.js +++ b/spec/frontend/error_tracking/components/error_details_spec.js @@ -1,10 +1,15 @@ import { createLocalVue, shallowMount } from '@vue/test-utils'; import Vuex from 'vuex'; +import { __ } from '~/locale'; import { GlLoadingIcon, GlLink, GlBadge, GlFormInput } from '@gitlab/ui'; import LoadingButton from '~/vue_shared/components/loading_button.vue'; import Stacktrace from '~/error_tracking/components/stacktrace.vue'; import ErrorDetails from '~/error_tracking/components/error_details.vue'; -import { severityLevel, severityLevelVariant } from '~/error_tracking/components/constants'; +import { + severityLevel, + severityLevelVariant, + errorStatus, +} from '~/error_tracking/components/constants'; const localVue = createLocalVue(); localVue.use(Vuex); @@ -56,6 +61,8 @@ describe('ErrorDetails', () => { actions = { startPollingDetails: () => {}, startPollingStacktrace: () => {}, + updateIgnoreStatus: jest.fn(), + updateResolveStatus: jest.fn(), }; getters = { @@ -219,6 +226,96 @@ describe('ErrorDetails', () => { }); }); + describe('Status update', () => { + const findUpdateIgnoreStatusButton = () => + wrapper.find('[data-qa-selector="update_ignore_status_button"]'); + const findUpdateResolveStatusButton = () => + wrapper.find('[data-qa-selector="update_resolve_status_button"]'); + + afterEach(() => { + actions.updateIgnoreStatus.mockClear(); + actions.updateResolveStatus.mockClear(); + }); + + describe('when error is unresolved', () => { + beforeEach(() => { + store.state.details.errorStatus = errorStatus.UNRESOLVED; + mountComponent(); + }); + + it('displays Ignore and Resolve buttons', () => { + expect(findUpdateIgnoreStatusButton().text()).toBe(__('Ignore')); + expect(findUpdateResolveStatusButton().text()).toBe(__('Resolve')); + }); + + it('marks error as ignored when ignore button is clicked', () => { + findUpdateIgnoreStatusButton().trigger('click'); + expect(actions.updateIgnoreStatus.mock.calls[0][1]).toEqual( + expect.objectContaining({ status: errorStatus.IGNORED }), + ); + }); + + it('marks error as resolved when resolve button is clicked', () => { + findUpdateResolveStatusButton().trigger('click'); + expect(actions.updateResolveStatus.mock.calls[0][1]).toEqual( + expect.objectContaining({ status: errorStatus.RESOLVED }), + ); + }); + }); + + describe('when error is ignored', () => { + beforeEach(() => { + store.state.details.errorStatus = errorStatus.IGNORED; + mountComponent(); + }); + + it('displays Undo Ignore and Resolve buttons', () => { + expect(findUpdateIgnoreStatusButton().text()).toBe(__('Undo ignore')); + expect(findUpdateResolveStatusButton().text()).toBe(__('Resolve')); + }); + + it('marks error as unresolved when ignore button is clicked', () => { + findUpdateIgnoreStatusButton().trigger('click'); + expect(actions.updateIgnoreStatus.mock.calls[0][1]).toEqual( + expect.objectContaining({ status: errorStatus.UNRESOLVED }), + ); + }); + + it('marks error as resolved when resolve button is clicked', () => { + findUpdateResolveStatusButton().trigger('click'); + expect(actions.updateResolveStatus.mock.calls[0][1]).toEqual( + expect.objectContaining({ status: errorStatus.RESOLVED }), + ); + }); + }); + + describe('when error is resolved', () => { + beforeEach(() => { + store.state.details.errorStatus = errorStatus.RESOLVED; + mountComponent(); + }); + + it('displays Ignore and Unresolve buttons', () => { + expect(findUpdateIgnoreStatusButton().text()).toBe(__('Ignore')); + expect(findUpdateResolveStatusButton().text()).toBe(__('Unresolve')); + }); + + it('marks error as ignored when ignore button is clicked', () => { + findUpdateIgnoreStatusButton().trigger('click'); + expect(actions.updateIgnoreStatus.mock.calls[0][1]).toEqual( + expect.objectContaining({ status: errorStatus.IGNORED }), + ); + }); + + it('marks error as unresolved when unresolve button is clicked', () => { + findUpdateResolveStatusButton().trigger('click'); + expect(actions.updateResolveStatus.mock.calls[0][1]).toEqual( + expect.objectContaining({ status: errorStatus.UNRESOLVED }), + ); + }); + }); + }); + describe('GitLab issue link', () => { const gitlabIssue = 'https://gitlab.example.com/issues/1'; const findGitLabLink = () => wrapper.find(`[href="${gitlabIssue}"]`); diff --git a/spec/frontend/error_tracking/store/actions_spec.js b/spec/frontend/error_tracking/store/actions_spec.js index 8bc53d94345..e4a895902b3 100644 --- a/spec/frontend/error_tracking/store/actions_spec.js +++ b/spec/frontend/error_tracking/store/actions_spec.js @@ -10,6 +10,8 @@ jest.mock('~/flash.js'); jest.mock('~/lib/utils/url_utility'); let mock; +const commit = jest.fn(); +const dispatch = jest.fn().mockResolvedValue(); describe('Sentry common store actions', () => { beforeEach(() => { @@ -20,26 +22,22 @@ describe('Sentry common store actions', () => { mock.restore(); createFlash.mockClear(); }); + const endpoint = '123/stacktrace'; + const redirectUrl = '/list'; + const status = 'resolved'; + const params = { endpoint, redirectUrl, status }; describe('updateStatus', () => { - const endpoint = '123/stacktrace'; - const redirectUrl = '/list'; - const status = 'resolved'; - it('should handle successful status update', done => { mock.onPut().reply(200, {}); testAction( actions.updateStatus, - { endpoint, redirectUrl, status }, + params, {}, [ { - payload: true, - type: types.SET_UPDATING_RESOLVE_STATUS, - }, - { - payload: false, - type: 'SET_UPDATING_RESOLVE_STATUS', + payload: 'resolved', + type: types.SET_ERROR_STATUS, }, ], [], @@ -52,27 +50,29 @@ describe('Sentry common store actions', () => { it('should handle unsuccessful status update', done => { mock.onPut().reply(400, {}); - testAction( - actions.updateStatus, - { endpoint, redirectUrl, status }, - {}, - [ - { - payload: true, - type: types.SET_UPDATING_RESOLVE_STATUS, - }, - { - payload: false, - type: types.SET_UPDATING_RESOLVE_STATUS, - }, - ], - [], - () => { - expect(visitUrl).not.toHaveBeenCalled(); - expect(createFlash).toHaveBeenCalledTimes(1); - done(); - }, - ); + testAction(actions.updateStatus, params, {}, [], [], () => { + expect(visitUrl).not.toHaveBeenCalled(); + expect(createFlash).toHaveBeenCalledTimes(1); + done(); + }); }); }); + + describe('updateResolveStatus', () => { + it('handles status update', () => + actions.updateResolveStatus({ commit, dispatch }, params).then(() => { + expect(commit).toHaveBeenCalledWith(types.SET_UPDATING_RESOLVE_STATUS, true); + expect(commit).toHaveBeenCalledWith(types.SET_UPDATING_RESOLVE_STATUS, false); + expect(dispatch).toHaveBeenCalledWith('updateStatus', params); + })); + }); + + describe('updateIgnoreStatus', () => { + it('handles status update', () => + actions.updateIgnoreStatus({ commit, dispatch }, params).then(() => { + expect(commit).toHaveBeenCalledWith(types.SET_UPDATING_IGNORE_STATUS, true); + expect(commit).toHaveBeenCalledWith(types.SET_UPDATING_IGNORE_STATUS, false); + expect(dispatch).toHaveBeenCalledWith('updateStatus', params); + })); + }); }); diff --git a/spec/graphql/types/error_tracking/sentry_error_collection_type_spec.rb b/spec/graphql/types/error_tracking/sentry_error_collection_type_spec.rb index 1e6b7f89c08..3de0a359c15 100644 --- a/spec/graphql/types/error_tracking/sentry_error_collection_type_spec.rb +++ b/spec/graphql/types/error_tracking/sentry_error_collection_type_spec.rb @@ -12,6 +12,7 @@ describe GitlabSchema.types['SentryErrorCollection'] do errors detailed_error external_url + error_stack_trace ] is_expected.to have_graphql_fields(*expected_fields) diff --git a/spec/graphql/types/error_tracking/sentry_error_stack_trace_entry_type_spec.rb b/spec/graphql/types/error_tracking/sentry_error_stack_trace_entry_type_spec.rb new file mode 100644 index 00000000000..ce5fade6fcc --- /dev/null +++ b/spec/graphql/types/error_tracking/sentry_error_stack_trace_entry_type_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['SentryErrorStackTraceEntry'] do + it { expect(described_class.graphql_name).to eq('SentryErrorStackTraceEntry') } + + it 'exposes the expected fields' do + expected_fields = %i[ + function + col + line + file_name + trace_context + ] + + is_expected.to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/error_tracking/sentry_error_stack_trace_type_spec.rb b/spec/graphql/types/error_tracking/sentry_error_stack_trace_type_spec.rb new file mode 100644 index 00000000000..ac41e6903e5 --- /dev/null +++ b/spec/graphql/types/error_tracking/sentry_error_stack_trace_type_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['SentryErrorStackTrace'] do + it { expect(described_class.graphql_name).to eq('SentryErrorStackTrace') } + + it { expect(described_class).to require_graphql_authorizations(:read_sentry_issue) } + + it 'exposes the expected fields' do + expected_fields = %i[ + issue_id + date_received + stack_trace_entries + ] + + is_expected.to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/helpers/projects/error_tracking_helper_spec.rb b/spec/helpers/projects/error_tracking_helper_spec.rb index 325ff32dd89..d4dcd7eb54e 100644 --- a/spec/helpers/projects/error_tracking_helper_spec.rb +++ b/spec/helpers/projects/error_tracking_helper_spec.rb @@ -83,7 +83,6 @@ describe Projects::ErrorTrackingHelper do describe '#error_details_data' do let(:issue_id) { 1234 } let(:route_params) { [project.owner, project, issue_id, { format: :json }] } - let(:list_path) { project_error_tracking_index_path(project) } 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) } @@ -91,10 +90,6 @@ describe Projects::ErrorTrackingHelper do let(:result) { helper.error_details_data(project, issue_id) } - it 'returns the correct list path' do - expect(result['list-path']).to eq list_path - end - it 'returns the correct issue id' do expect(result['issue-id']).to eq issue_id end diff --git a/spec/lib/gitlab/mail_room/mail_room_spec.rb b/spec/lib/gitlab/mail_room/mail_room_spec.rb index 43218fc6e0d..5d41ee06263 100644 --- a/spec/lib/gitlab/mail_room/mail_room_spec.rb +++ b/spec/lib/gitlab/mail_room/mail_room_spec.rb @@ -4,9 +4,10 @@ require 'spec_helper' describe Gitlab::MailRoom do let(:default_port) { 143 } - let(:default_config) do + let(:yml_config) do { - enabled: false, + enabled: true, + address: 'address@example.com', port: default_port, ssl: false, start_tls: false, @@ -16,71 +17,73 @@ describe Gitlab::MailRoom do } end - shared_examples_for 'only truthy if both enabled and address are truthy' do |target_proc| - context 'with both enabled and address as truthy values' do - it 'is truthy' do - stub_config(enabled: true, address: 'localhost') + let(:custom_config) { {} } + let(:incoming_email_config) { yml_config.merge(custom_config) } + let(:service_desk_email_config) { yml_config.merge(custom_config) } - expect(target_proc.call).to be_truthy - end - end - - context 'with address only as truthy' do - it 'is falsey' do - stub_config(enabled: false, address: 'localhost') - - expect(target_proc.call).to be_falsey - end - end + let(:configs) do + { + incoming_email: incoming_email_config, + service_desk_email: service_desk_email_config + } + end - context 'with enabled only as truthy' do - it 'is falsey' do - stub_config(enabled: true, address: nil) + before do + described_class.instance_variable_set(:@enabled_configs, nil) + end - expect(target_proc.call).to be_falsey - end + describe '#enabled_configs' do + before do + allow(described_class).to receive(:load_yaml).and_return(configs) end - context 'with neither address nor enabled as truthy' do - it 'is falsey' do - stub_config(enabled: false, address: nil) - - expect(target_proc.call).to be_falsey + context 'when both email and address is set' do + it 'returns email configs' do + expect(described_class.enabled_configs.size).to eq(2) end end - end - - before do - described_class.reset_config! - allow(File).to receive(:exist?).and_return true - end - describe '#config' do - context 'if the yml file cannot be found' do + context 'when the yml file cannot be found' do before do - allow(File).to receive(:exist?).and_return false + allow(described_class).to receive(:config_file).and_return('not_existing_file') end - it 'returns an empty hash' do - expect(described_class.config).to be_empty + it 'returns an empty list' do + expect(described_class.enabled_configs).to be_empty end end - before do - allow(described_class).to receive(:load_from_yaml).and_return(default_config) + context 'when email is disabled' do + let(:custom_config) { { enabled: false } } + + it 'returns an empty list' do + expect(described_class.enabled_configs).to be_empty + end end - it 'sets up config properly' do - expected_result = default_config + context 'when email is enabled but address is not set' do + let(:custom_config) { { enabled: true, address: '' } } - expect(described_class.config).to match expected_result + it 'returns an empty list' do + expect(described_class.enabled_configs).to be_empty + end end context 'when a config value is missing from the yml file' do + let(:yml_config) { {} } + let(:custom_config) { { enabled: true, address: 'address@example.com' } } + it 'overwrites missing values with the default' do - stub_config(port: nil) + expect(described_class.enabled_configs.first[:port]).to eq(Gitlab::MailRoom::DEFAULT_CONFIG[:port]) + end + end + + context 'when only incoming_email config is present' do + let(:configs) { { incoming_email: incoming_email_config } } - expect(described_class.config[:port]).to eq default_port + it 'returns only encoming_email' do + expect(described_class.enabled_configs.size).to eq(1) + expect(described_class.enabled_configs.first[:worker]).to eq('EmailReceiverWorker') end end @@ -91,50 +94,31 @@ describe Gitlab::MailRoom do allow(Gitlab::Redis::Queues).to receive(:new).and_return(fake_redis_queues) end - target_proc = proc { described_class.config[:redis_url] } + it 'sets redis config' do + config = described_class.enabled_configs.first - it_behaves_like 'only truthy if both enabled and address are truthy', target_proc + expect(config[:redis_url]).to eq('localhost') + expect(config[:sentinels]).to eq('yes, them') + end end describe 'setting up the log path' do context 'if the log path is a relative path' do - it 'expands the log path to an absolute value' do - stub_config(log_path: 'tiny_log.log') + let(:custom_config) { { log_path: 'tiny_log.log' } } - new_path = Pathname.new(described_class.config[:log_path]) + it 'expands the log path to an absolute value' do + new_path = Pathname.new(described_class.enabled_configs.first[:log_path]) expect(new_path.absolute?).to be_truthy end end context 'if the log path is absolute path' do - it 'leaves the path as-is' do - new_path = '/dev/null' - stub_config(log_path: new_path) + let(:custom_config) { { log_path: '/dev/null' } } - expect(described_class.config[:log_path]).to eq new_path + it 'leaves the path as-is' do + expect(described_class.enabled_configs.first[:log_path]).to eq '/dev/null' end end end end - - describe '#enabled?' do - target_proc = proc { described_class.enabled? } - - it_behaves_like 'only truthy if both enabled and address are truthy', target_proc - end - - describe '#reset_config?' do - it 'resets config' do - described_class.instance_variable_set(:@config, { some_stuff: 'hooray' }) - - described_class.reset_config! - - expect(described_class.instance_variable_get(:@config)).to be_nil - end - end - - def stub_config(override_values) - modified_config = default_config.merge(override_values) - allow(described_class).to receive(:load_from_yaml).and_return(modified_config) - end end diff --git a/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb b/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb index e68025bf01b..06a0bfc0d32 100644 --- a/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb +++ b/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb @@ -40,8 +40,8 @@ describe 'sentry errors requests' do post_graphql(query, current_user: current_user) end - it "is expected to return an empty error" do - expect(error_data).to eq nil + it 'is expected to return an empty error' do + expect(error_data).to be_nil end end @@ -49,7 +49,7 @@ describe 'sentry errors requests' do before do allow_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting) .to receive(:issue_details) - .and_return({ issue: sentry_detailed_error }) + .and_return(issue: sentry_detailed_error) post_graphql(query, current_user: current_user) end @@ -72,8 +72,8 @@ describe 'sentry errors requests' do context 'user does not have permission' do let(:current_user) { create(:user) } - it "is expected to return an empty error" do - expect(error_data).to eq nil + it 'is expected to return an empty error' do + expect(error_data).to be_nil end end end @@ -82,13 +82,13 @@ describe 'sentry errors requests' do before do expect_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting) .to receive(:issue_details) - .and_return({ error: 'error message' }) + .and_return(error: 'error message') post_graphql(query, current_user: current_user) end it 'is expected to handle the error and return nil' do - expect(error_data).to eq nil + expect(error_data).to be_nil end end end @@ -132,8 +132,8 @@ describe 'sentry errors requests' do post_graphql(query, current_user: current_user) end - it "is expected to return nil" do - expect(error_data).to eq nil + it 'is expected to return nil' do + expect(error_data).to be_nil end end @@ -141,7 +141,7 @@ describe 'sentry errors requests' do before do expect_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting) .to receive(:list_sentry_issues) - .and_return({ issues: [sentry_error], pagination: pagination }) + .and_return(issues: [sentry_error], pagination: pagination) post_graphql(query, current_user: current_user) end @@ -174,17 +174,82 @@ describe 'sentry errors requests' do end end - context "sentry api itself errors out" do + context 'sentry api itself errors out' do before do expect_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting) .to receive(:list_sentry_issues) - .and_return({ error: 'error message' }) + .and_return(error: 'error message') post_graphql(query, current_user: current_user) end it 'is expected to handle the error and return nil' do - expect(error_data).to eq nil + expect(error_data).to be_nil + end + end + end + + describe 'getting a stack trace' do + let_it_be(:sentry_stack_trace) { build(:error_tracking_error_event) } + let(:sentry_gid) { Gitlab::ErrorTracking::DetailedError.new(id: 1).to_global_id.to_s } + + let(:stack_trace_fields) do + all_graphql_fields_for('SentryErrorStackTrace'.classify) + end + + let(:fields) do + query_graphql_field('errorStackTrace', { id: sentry_gid }, stack_trace_fields) + end + + let(:stack_trace_data) { graphql_data.dig('project', 'sentryErrors', 'errorStackTrace') } + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: current_user) + end + end + + context 'when data is loading via reactive cache' do + before do + post_graphql(query, current_user: current_user) + end + + it 'is expected to return an empty error' do + expect(stack_trace_data).to be_nil + end + end + + context 'reactive cache returns data' do + before do + allow_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting) + .to receive(:issue_latest_event) + .and_return(latest_event: sentry_stack_trace) + + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'setting stack trace error' + + context 'user does not have permission' do + let(:current_user) { create(:user) } + + it 'is expected to return an empty error' do + expect(stack_trace_data).to be_nil + end + end + end + + context 'sentry api returns an error' do + before do + expect_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting) + .to receive(:issue_latest_event) + .and_return(error: 'error message') + + post_graphql(query, current_user: current_user) + end + + it 'is expected to handle the error and return nil' do + expect(stack_trace_data).to be_nil end end end diff --git a/spec/support/shared_examples/error_tracking_shared_examples.rb b/spec/support/shared_examples/error_tracking_shared_examples.rb index 86134fa7fd1..8e7a63b69c7 100644 --- a/spec/support/shared_examples/error_tracking_shared_examples.rb +++ b/spec/support/shared_examples/error_tracking_shared_examples.rb @@ -3,11 +3,34 @@ RSpec.shared_examples 'setting sentry error data' do it 'sets the sentry error data correctly' do aggregate_failures 'testing the sentry error is correct' do - expect(error['id']).to eql sentry_error.to_global_id.to_s - expect(error['sentryId']).to eql sentry_error.id.to_s - expect(error['status']).to eql sentry_error.status.upcase - expect(error['firstSeen']).to eql sentry_error.first_seen - expect(error['lastSeen']).to eql sentry_error.last_seen + expect(error['id']).to eq sentry_error.to_global_id.to_s + expect(error['sentryId']).to eq sentry_error.id.to_s + expect(error['status']).to eq sentry_error.status.upcase + expect(error['firstSeen']).to eq sentry_error.first_seen + expect(error['lastSeen']).to eq sentry_error.last_seen + end + end +end + +RSpec.shared_examples 'setting stack trace error' do + it 'sets the stack trace data correctly' do + aggregate_failures 'testing the stack trace is correct' do + expect(stack_trace_data['dateReceived']).to eq(sentry_stack_trace.date_received) + expect(stack_trace_data['issueId']).to eq(sentry_stack_trace.issue_id) + expect(stack_trace_data['stackTraceEntries']).to be_an_instance_of(Array) + expect(stack_trace_data['stackTraceEntries'].size).to eq(sentry_stack_trace.stack_trace_entries.size) + end + end + + it 'sets the stack trace entry data correctly' do + aggregate_failures 'testing the stack trace entry is correct' do + stack_trace_entry = stack_trace_data['stackTraceEntries'].first + model_entry = sentry_stack_trace.stack_trace_entries.first + + expect(stack_trace_entry['function']).to eq model_entry['function'] + expect(stack_trace_entry['col']).to eq model_entry['colNo'] + expect(stack_trace_entry['line']).to eq model_entry['lineNo'].to_s + expect(stack_trace_entry['fileName']).to eq model_entry['filename'] end end end -- 2.30.9