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