Commit d5d3c035 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 0434f38e
...@@ -184,7 +184,7 @@ GEM ...@@ -184,7 +184,7 @@ GEM
unicode_utils (~> 1.4) unicode_utils (~> 1.4)
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
crass (1.0.5) crass (1.0.6)
creole (0.5.0) creole (0.5.0)
css_parser (1.7.0) css_parser (1.7.0)
addressable addressable
...@@ -526,7 +526,7 @@ GEM ...@@ -526,7 +526,7 @@ GEM
mime-types (~> 3.0) mime-types (~> 3.0)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.7.0) i18n (1.8.2)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
i18n_data (0.8.0) i18n_data (0.8.0)
icalendar (2.4.1) icalendar (2.4.1)
......
...@@ -13,3 +13,9 @@ export const severityLevelVariant = { ...@@ -13,3 +13,9 @@ export const severityLevelVariant = {
[severityLevel.INFO]: 'info', [severityLevel.INFO]: 'info',
[severityLevel.DEBUG]: 'light', [severityLevel.DEBUG]: 'light',
}; };
export const errorStatus = {
IGNORED: 'ignored',
RESOLVED: 'resolved',
UNRESOLVED: 'unresolved',
};
...@@ -11,7 +11,7 @@ import Stacktrace from './stacktrace.vue'; ...@@ -11,7 +11,7 @@ import Stacktrace from './stacktrace.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event'; import TrackEventDirective from '~/vue_shared/directives/track_event';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import { trackClickErrorLinkToSentryOptions } from '../utils'; import { trackClickErrorLinkToSentryOptions } from '../utils';
import { severityLevel, severityLevelVariant } from './constants'; import { severityLevel, severityLevelVariant, errorStatus } from './constants';
import query from '../queries/details.query.graphql'; import query from '../queries/details.query.graphql';
...@@ -32,10 +32,6 @@ export default { ...@@ -32,10 +32,6 @@ export default {
}, },
mixins: [timeagoMixin], mixins: [timeagoMixin],
props: { props: {
listPath: {
type: String,
required: true,
},
issueUpdatePath: { issueUpdatePath: {
type: String, type: String,
required: true, required: true,
...@@ -80,6 +76,7 @@ export default { ...@@ -80,6 +76,7 @@ export default {
result(res) { result(res) {
if (res.data.project?.sentryDetailedError) { if (res.data.project?.sentryDetailedError) {
this.$apollo.queries.GQLerror.stopPolling(); this.$apollo.queries.GQLerror.stopPolling();
this.setStatus(this.GQLerror.status);
} }
}, },
}, },
...@@ -98,6 +95,7 @@ export default { ...@@ -98,6 +95,7 @@ export default {
'stacktraceData', 'stacktraceData',
'updatingResolveStatus', 'updatingResolveStatus',
'updatingIgnoreStatus', 'updatingIgnoreStatus',
'errorStatus',
]), ]),
...mapGetters('details', ['stacktrace']), ...mapGetters('details', ['stacktrace']),
reported() { reported() {
...@@ -153,20 +151,40 @@ export default { ...@@ -153,20 +151,40 @@ export default {
severityLevelVariant[this.error.tags.level] || severityLevelVariant[severityLevel.ERROR] 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() { mounted() {
this.startPollingDetails(this.issueDetailsPath); this.startPollingDetails(this.issueDetailsPath);
this.startPollingStacktrace(this.issueStackTracePath); this.startPollingStacktrace(this.issueStackTracePath);
}, },
methods: { methods: {
...mapActions('details', ['startPollingDetails', 'startPollingStacktrace', 'updateStatus']), ...mapActions('details', [
'startPollingDetails',
'startPollingStacktrace',
'updateStatus',
'setStatus',
'updateResolveStatus',
'updateIgnoreStatus',
]),
trackClickErrorLinkToSentryOptions, trackClickErrorLinkToSentryOptions,
createIssue() { createIssue() {
this.issueCreationInProgress = true; this.issueCreationInProgress = true;
this.$refs.sentryIssueForm.submit(); this.$refs.sentryIssueForm.submit();
}, },
updateIssueStatus(status) { onIgnoreStatusUpdate() {
this.updateStatus({ endpoint: this.issueUpdatePath, redirectUrl: this.listPath, status }); 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) { formatDate(date) {
return `${this.timeFormatted(date)} (${dateFormat(date, 'UTC:yyyy-mm-dd h:MM:ssTT Z')})`; return `${this.timeFormatted(date)} (${dateFormat(date, 'UTC:yyyy-mm-dd h:MM:ssTT Z')})`;
...@@ -185,15 +203,17 @@ export default { ...@@ -185,15 +203,17 @@ export default {
<span v-if="!loadingStacktrace && stacktrace" v-html="reported"></span> <span v-if="!loadingStacktrace && stacktrace" v-html="reported"></span>
<div class="d-inline-flex"> <div class="d-inline-flex">
<loading-button <loading-button
:label="__('Ignore')" :label="ignoreBtnLabel"
:loading="updatingIgnoreStatus" :loading="updatingIgnoreStatus"
@click="updateIssueStatus('ignored')" data-qa-selector="update_ignore_status_button"
@click="onIgnoreStatusUpdate"
/> />
<loading-button <loading-button
class="btn-outline-info ml-2" class="btn-outline-info ml-2"
:label="__('Resolve')" :label="resolveBtnLabel"
:loading="updatingResolveStatus" :loading="updatingResolveStatus"
@click="updateIssueStatus('resolved')" data-qa-selector="update_resolve_status_button"
@click="onResolveStatusUpdate"
/> />
<gl-button <gl-button
v-if="error.gitlab_issue" v-if="error.gitlab_issue"
......
...@@ -25,7 +25,6 @@ export default () => { ...@@ -25,7 +25,6 @@ export default () => {
const { const {
issueId, issueId,
projectPath, projectPath,
listPath,
issueUpdatePath, issueUpdatePath,
issueDetailsPath, issueDetailsPath,
issueStackTracePath, issueStackTracePath,
...@@ -36,7 +35,6 @@ export default () => { ...@@ -36,7 +35,6 @@ export default () => {
props: { props: {
issueId, issueId,
projectPath, projectPath,
listPath,
issueUpdatePath, issueUpdatePath,
issueDetailsPath, issueDetailsPath,
issueStackTracePath, issueStackTracePath,
......
...@@ -6,6 +6,7 @@ query errorDetails($fullPath: ID!, $errorId: ID!) { ...@@ -6,6 +6,7 @@ query errorDetails($fullPath: ID!, $errorId: ID!) {
title title
userCount userCount
count count
status
firstSeen firstSeen
lastSeen lastSeen
message message
......
...@@ -4,16 +4,33 @@ import createFlash from '~/flash'; ...@@ -4,16 +4,33 @@ import createFlash from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale'; import { __ } from '~/locale';
export function updateStatus({ commit }, { endpoint, redirectUrl, status }) { export const setStatus = ({ commit }, status) => {
const type = commit(types.SET_ERROR_STATUS, status.toLowerCase());
status === 'resolved' ? types.SET_UPDATING_RESOLVE_STATUS : types.SET_UPDATING_IGNORE_STATUS; };
commit(type, true);
return service export const updateStatus = ({ commit }, { endpoint, redirectUrl, status }) =>
service
.updateErrorStatus(endpoint, status) .updateErrorStatus(endpoint, status)
.then(() => visitUrl(redirectUrl)) .then(() => {
.catch(() => createFlash(__('Failed to update issue status'))) if (redirectUrl) visitUrl(redirectUrl);
.finally(() => commit(type, false)); 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 () => {}; export default () => {};
...@@ -5,4 +5,5 @@ export default () => ({ ...@@ -5,4 +5,5 @@ export default () => ({
loadingStacktrace: true, loadingStacktrace: true,
updatingResolveStatus: false, updatingResolveStatus: false,
updatingIgnoreStatus: false, updatingIgnoreStatus: false,
errorStatus: '',
}); });
export const SET_UPDATING_RESOLVE_STATUS = 'SET_UPDATING_RESOLVE_STATUS'; export const SET_UPDATING_RESOLVE_STATUS = 'SET_UPDATING_RESOLVE_STATUS';
export const SET_UPDATING_IGNORE_STATUS = 'SET_UPDATING_IGNORE_STATUS'; export const SET_UPDATING_IGNORE_STATUS = 'SET_UPDATING_IGNORE_STATUS';
export const SET_ERROR_STATUS = 'SET_ERROR_STATUS';
...@@ -7,4 +7,7 @@ export default { ...@@ -7,4 +7,7 @@ export default {
[types.SET_UPDATING_RESOLVE_STATUS](state, updating) { [types.SET_UPDATING_RESOLVE_STATUS](state, updating) {
state.updatingResolveStatus = updating; state.updatingResolveStatus = updating;
}, },
[types.SET_ERROR_STATUS](state, status) {
state.errorStatus = status;
},
}; };
# 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
...@@ -28,6 +28,10 @@ module Types ...@@ -28,6 +28,10 @@ module Types
null: true, null: true,
description: 'Detailed version of a Sentry error on the project', description: 'Detailed version of a Sentry error on the project',
resolver: Resolvers::ErrorTracking::SentryDetailedErrorResolver 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, field :external_url,
GraphQL::STRING_TYPE, GraphQL::STRING_TYPE,
null: true, null: true,
......
# 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
# 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
# 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
...@@ -22,7 +22,6 @@ module Projects::ErrorTrackingHelper ...@@ -22,7 +22,6 @@ module Projects::ErrorTrackingHelper
{ {
'issue-id' => issue_id, 'issue-id' => issue_id,
'project-path' => project.full_path, 'project-path' => project.full_path,
'list-path' => project_error_tracking_index_path(project),
'issue-details-path' => details_project_error_tracking_index_path(*opts), 'issue-details-path' => details_project_error_tracking_index_path(*opts),
'issue-update-path' => update_project_error_tracking_index_path(*opts), 'issue-update-path' => update_project_error_tracking_index_path(*opts),
'project-issues-path' => project_issues_path(project), 'project-issues-path' => project_issues_path(project),
......
.top-area .top-area
%ul.nav-links.nav.nav-tabs %ul.nav-links.nav.nav-tabs
= nav_link(page: [trending_explore_projects_path, explore_root_path]) do = nav_link(page: [explore_projects_path, explore_root_path]) do
= link_to trending_explore_projects_path do = link_to explore_projects_path do
= _('Trending') = _('All')
= nav_link(page: starred_explore_projects_path) do = nav_link(page: starred_explore_projects_path) do
= link_to starred_explore_projects_path do = link_to starred_explore_projects_path do
= _('Most stars') = _('Most stars')
= nav_link(page: explore_projects_path) do = nav_link(page: trending_explore_projects_path) do
= link_to explore_projects_path do = link_to trending_explore_projects_path do
= _('All') = _('Trending')
.nav-controls .nav-controls
- unless current_user - unless current_user
......
---
title: Make Explore Projects default to All
merge_request: 23811
author:
type: changed
---
title: Reverse actions for resolve/ignore Sentry issue
merge_request: 23516
author:
type: added
---
title: Add Sentry error stack trace to GraphQL API
merge_request: 23750
author:
type: added
---
title: Separate snippet entities into own class files
merge_request: 24183
author: Rajendra Kadam
type: added
:mailboxes: :mailboxes:
<% <%
require_relative "../lib/gitlab/mail_room" unless defined?(Gitlab::MailRoom) require_relative "../lib/gitlab/mail_room" unless defined?(Gitlab::MailRoom)
config = Gitlab::MailRoom.config Gitlab::MailRoom.enabled_configs.each do |config|
if Gitlab::MailRoom.enabled?
%> %>
- -
:host: <%= config[:host].to_json %> :host: <%= config[:host].to_json %>
...@@ -24,8 +22,8 @@ ...@@ -24,8 +22,8 @@
:delivery_options: :delivery_options:
:redis_url: <%= config[:redis_url].to_json %> :redis_url: <%= config[:redis_url].to_json %>
:namespace: <%= Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE %> :namespace: <%= Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE %>
:queue: email_receiver :queue: <%= config[:queue] %>
:worker: EmailReceiverWorker :worker: <%= config[:worker] %>
<% if config[:sentinels] %> <% if config[:sentinels] %>
:sentinels: :sentinels:
<% config[:sentinels].each do |sentinel| %> <% config[:sentinels].each do |sentinel| %>
......
...@@ -8,7 +8,7 @@ namespace :explore do ...@@ -8,7 +8,7 @@ namespace :explore do
resources :groups, only: [:index] resources :groups, only: [:index]
resources :snippets, only: [:index] resources :snippets, only: [:index]
root to: 'projects#trending' root to: 'projects#index'
end end
# Compatibility with old routing # Compatibility with old routing
......
...@@ -224,6 +224,8 @@ ...@@ -224,6 +224,8 @@
- 2 - 2
- - self_monitoring_project_delete - - self_monitoring_project_delete
- 2 - 2
- - service_desk_email_receiver
- 1
- - system_hook_push - - system_hook_push
- 1 - 1
- - todos_destroyer - - todos_destroyer
......
...@@ -200,7 +200,7 @@ with the added complexity of many more nodes to configure, manage, and monitor. ...@@ -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) ![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 The Support and Quality teams build, performance test, and validate Reference
Architectures that support large numbers of users. The specifications below are Architectures that support large numbers of users. The specifications below are
......
...@@ -6298,6 +6298,16 @@ type SentryErrorCollection { ...@@ -6298,6 +6298,16 @@ type SentryErrorCollection {
id: ID! id: ID!
): SentryDetailedError ): SentryDetailedError
"""
Stack Trace of Sentry Error
"""
errorStackTrace(
"""
ID of the Sentry issue
"""
id: ID!
): SentryErrorStackTrace
""" """
Collection of Sentry Errors Collection of Sentry Errors
""" """
...@@ -6386,6 +6396,71 @@ type SentryErrorFrequency { ...@@ -6386,6 +6396,71 @@ type SentryErrorFrequency {
time: Time! 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 State of a Sentry error
""" """
......
...@@ -17454,6 +17454,33 @@ ...@@ -17454,6 +17454,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "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", "name": "errors",
"description": "Collection of Sentry Errors", "description": "Collection of Sentry Errors",
...@@ -17984,6 +18011,221 @@ ...@@ -17984,6 +18011,221 @@
"enumValues": null, "enumValues": null,
"possibleTypes": 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", "kind": "OBJECT",
"name": "Metadata", "name": "Metadata",
......
...@@ -983,6 +983,7 @@ An object containing a collection of Sentry errors, and a detailed error. ...@@ -983,6 +983,7 @@ An object containing a collection of Sentry errors, and a detailed error.
| Name | Type | Description | | Name | Type | Description |
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `detailedError` | SentryDetailedError | Detailed version of a Sentry error on the project | | `detailedError` | SentryDetailedError | Detailed version of a Sentry error on the project |
| `errorStackTrace` | SentryErrorStackTrace | Stack Trace of Sentry Error |
| `errors` | SentryErrorConnection | Collection of Sentry Errors | | `errors` | SentryErrorConnection | Collection of Sentry Errors |
| `externalUrl` | String | External URL for Sentry | | `externalUrl` | String | External URL for Sentry |
...@@ -993,6 +994,37 @@ An object containing a collection of Sentry errors, and a detailed error. ...@@ -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 | | `count` | Int! | Count of errors received since the previously recorded time |
| `time` | Time! | Time the error frequency stats were recorded | | `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 ## SentryErrorTags
State of a Sentry error State of a Sentry error
......
...@@ -128,75 +128,6 @@ module API ...@@ -128,75 +128,6 @@ module API
end end
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 class IssuableEntity < Grape::Entity
expose :id, :iid expose :id, :iid
expose(:project_id) { |entity| entity&.project.try(:id) } expose(:project_id) { |entity| entity&.project.try(:id) }
......
# frozen_string_literal: true
module API
module Entities
class BasicRef < Grape::Entity
expose :type, :name
end
end
end
# 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
# 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
# frozen_String_literal: true
module API
module Entities
class ProjectSnippet < Entities::Snippet
end
end
end
# 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
# 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
...@@ -5,7 +5,11 @@ module Gitlab ...@@ -5,7 +5,11 @@ module Gitlab
class ErrorEvent class ErrorEvent
include ActiveModel::Model 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 end
end end
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
require 'yaml' require 'yaml'
require 'json' require 'json'
require 'pathname'
require_relative 'redis/queues' unless defined?(Gitlab::Redis::Queues) require_relative 'redis/queues' unless defined?(Gitlab::Redis::Queues)
# This service is run independently of the main Rails process, # This service is run independently of the main Rails process,
...@@ -21,39 +22,60 @@ module Gitlab ...@@ -21,39 +22,60 @@ module Gitlab
log_path: RAILS_ROOT_DIR.join('log', 'mail_room_json.log') log_path: RAILS_ROOT_DIR.join('log', 'mail_room_json.log')
}.freeze }.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 class << self
def enabled? def enabled_configs
config[:enabled] && config[:address] @enabled_configs ||= configs.select { |config| enabled?(config) }
end end
def config private
@config ||= fetch_config
end
def reset_config! def enabled?(config)
@config = nil config[:enabled] && !config[:address].to_s.empty?
end 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) return {} unless File.exist?(config_file)
config = load_from_yaml || {} config = merged_configs(config_key)
config = DEFAULT_CONFIG.merge(config) do |_key, oldval, newval| 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 newval.nil? ? oldval : newval
end end
end
if config[:enabled] && config[:address] def redis_config
gitlab_redis_queues = Gitlab::Redis::Queues.new(rails_env) gitlab_redis_queues = Gitlab::Redis::Queues.new(rails_env)
config[:redis_url] = gitlab_redis_queues.url config = { redis_url: gitlab_redis_queues.url }
if gitlab_redis_queues.sentinels? if gitlab_redis_queues.sentinels?
config[:sentinels] = gitlab_redis_queues.sentinels config[:sentinels] = gitlab_redis_queues.sentinels
end
end end
config[:log_path] = File.expand_path(config[:log_path], RAILS_ROOT_DIR)
config config
end end
...@@ -65,8 +87,8 @@ module Gitlab ...@@ -65,8 +87,8 @@ module Gitlab
ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] || File.expand_path('../../config/gitlab.yml', __dir__) ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] || File.expand_path('../../config/gitlab.yml', __dir__)
end end
def load_from_yaml def load_yaml
YAML.load_file(config_file)[rails_env].deep_symbolize_keys[:incoming_email] @yaml ||= YAML.load_file(config_file)[rails_env].deep_symbolize_keys
end end
end end
end end
......
...@@ -20259,6 +20259,9 @@ msgstr "" ...@@ -20259,6 +20259,9 @@ msgstr ""
msgid "Undo" msgid "Undo"
msgstr "" msgstr ""
msgid "Undo ignore"
msgstr ""
msgid "Unfortunately, your email message to GitLab could not be processed." msgid "Unfortunately, your email message to GitLab could not be processed."
msgstr "" msgstr ""
...@@ -20310,6 +20313,9 @@ msgstr "" ...@@ -20310,6 +20313,9 @@ msgstr ""
msgid "Unmarks this %{noun} as Work In Progress." msgid "Unmarks this %{noun} as Work In Progress."
msgstr "" msgstr ""
msgid "Unresolve"
msgstr ""
msgid "Unresolve discussion" msgid "Unresolve discussion"
msgstr "" msgstr ""
......
...@@ -26,30 +26,19 @@ module QA ...@@ -26,30 +26,19 @@ module QA
group_id = fetch_group_id group_id = fetch_group_id
sub_groups_head_response = head Runtime::API::Request.new(@api_client, "/groups/#{group_id}/subgroups", per_page: "100").url 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] total_sub_group_pages = sub_groups_head_response.headers[:x_total_pages]
STDOUT.puts "total_sub_groups: #{total_sub_groups}" sub_group_ids = fetch_subgroup_ids(group_id, total_sub_group_pages)
STDOUT.puts "total_sub_group_pages: #{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| delete_subgroups(sub_group_ids) unless sub_group_ids.empty?
# 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
STDOUT.puts "\nDone" STDOUT.puts "\nDone"
end end
private private
def delete_subgroups(sub_group_ids) def delete_subgroups(sub_group_ids)
STDOUT.puts "Deleting #{sub_group_ids.length} subgroups..."
sub_group_ids.each do |subgroup_id| sub_group_ids.each do |subgroup_id|
delete_response = delete Runtime::API::Request.new(@api_client, "/groups/#{subgroup_id}").url 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" dot_or_f = delete_response.code == 202 ? "\e[32m.\e[0m" : "\e[31mF\e[0m"
...@@ -61,6 +50,17 @@ module QA ...@@ -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 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"] JSON.parse(group_search_response.body).first["id"]
end 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 end
end end
...@@ -39,39 +39,31 @@ describe 'mail_room.yml' do ...@@ -39,39 +39,31 @@ describe 'mail_room.yml' do
end end
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(:gitlab_config_path) { 'spec/fixtures/config/mail_room_enabled.yml' }
let(:queues_config_path) { 'spec/fixtures/config/redis_queues_new_format_host.yml' } let(:queues_config_path) { 'spec/fixtures/config/redis_queues_new_format_host.yml' }
let(:gitlab_redis_queues) { Gitlab::Redis::Queues.new(Rails.env) } let(:gitlab_redis_queues) { Gitlab::Redis::Queues.new(Rails.env) }
it 'contains the intended configuration' do it 'contains the intended configuration' do
expect(configuration[:mailboxes].length).to eq(1) expected_mailbox = {
mailbox = configuration[:mailboxes].first host: 'imap.gmail.com',
port: 993,
expect(mailbox[:host]).to eq('imap.gmail.com') ssl: true,
expect(mailbox[:port]).to eq(993) start_tls: false,
expect(mailbox[:ssl]).to eq(true) email: 'gitlab-incoming@gmail.com',
expect(mailbox[:start_tls]).to eq(false) password: '[REDACTED]',
expect(mailbox[:email]).to eq('gitlab-incoming@gmail.com') name: 'inbox',
expect(mailbox[:password]).to eq('[REDACTED]') idle_timeout: 60
expect(mailbox[:name]).to eq('inbox') }
expect(mailbox[:idle_timeout]).to eq(60) expected_options = {
redis_url: gitlab_redis_queues.url,
redis_url = gitlab_redis_queues.url sentinels: gitlab_redis_queues.sentinels
sentinels = gitlab_redis_queues.sentinels }
expect(mailbox[:delivery_options][:redis_url]).to be_present expect(configuration[:mailboxes].length).to eq(2)
expect(mailbox[:delivery_options][:redis_url]).to eq(redis_url) expect(configuration[:mailboxes]).to all(include(expected_mailbox))
expect(configuration[:mailboxes].map { |m| m[:delivery_options] }).to all(include(expected_options))
expect(mailbox[:delivery_options][:sentinels]).to be_present expect(configuration[:mailboxes].map { |m| m[:arbitration_options] }).to all(include(expected_options))
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)
end end
end end
......
...@@ -51,7 +51,7 @@ describe 'Dashboard shortcuts', :js do ...@@ -51,7 +51,7 @@ describe 'Dashboard shortcuts', :js do
find('body').send_keys([:shift, 'P']) find('body').send_keys([:shift, 'P'])
find('.nothing-here-block') 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
end end
......
...@@ -9,3 +9,14 @@ test: ...@@ -9,3 +9,14 @@ test:
ssl: true ssl: true
start_tls: false start_tls: false
mailbox: "inbox" 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"
...@@ -9,3 +9,14 @@ test: ...@@ -9,3 +9,14 @@ test:
ssl: true ssl: true
start_tls: false start_tls: false
mailbox: "inbox" 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"
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { __ } from '~/locale';
import { GlLoadingIcon, GlLink, GlBadge, GlFormInput } from '@gitlab/ui'; import { GlLoadingIcon, GlLink, GlBadge, GlFormInput } from '@gitlab/ui';
import LoadingButton from '~/vue_shared/components/loading_button.vue'; import LoadingButton from '~/vue_shared/components/loading_button.vue';
import Stacktrace from '~/error_tracking/components/stacktrace.vue'; import Stacktrace from '~/error_tracking/components/stacktrace.vue';
import ErrorDetails from '~/error_tracking/components/error_details.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(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
...@@ -56,6 +61,8 @@ describe('ErrorDetails', () => { ...@@ -56,6 +61,8 @@ describe('ErrorDetails', () => {
actions = { actions = {
startPollingDetails: () => {}, startPollingDetails: () => {},
startPollingStacktrace: () => {}, startPollingStacktrace: () => {},
updateIgnoreStatus: jest.fn(),
updateResolveStatus: jest.fn(),
}; };
getters = { getters = {
...@@ -219,6 +226,96 @@ describe('ErrorDetails', () => { ...@@ -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', () => { describe('GitLab issue link', () => {
const gitlabIssue = 'https://gitlab.example.com/issues/1'; const gitlabIssue = 'https://gitlab.example.com/issues/1';
const findGitLabLink = () => wrapper.find(`[href="${gitlabIssue}"]`); const findGitLabLink = () => wrapper.find(`[href="${gitlabIssue}"]`);
......
...@@ -10,6 +10,8 @@ jest.mock('~/flash.js'); ...@@ -10,6 +10,8 @@ jest.mock('~/flash.js');
jest.mock('~/lib/utils/url_utility'); jest.mock('~/lib/utils/url_utility');
let mock; let mock;
const commit = jest.fn();
const dispatch = jest.fn().mockResolvedValue();
describe('Sentry common store actions', () => { describe('Sentry common store actions', () => {
beforeEach(() => { beforeEach(() => {
...@@ -20,26 +22,22 @@ describe('Sentry common store actions', () => { ...@@ -20,26 +22,22 @@ describe('Sentry common store actions', () => {
mock.restore(); mock.restore();
createFlash.mockClear(); createFlash.mockClear();
}); });
const endpoint = '123/stacktrace';
const redirectUrl = '/list';
const status = 'resolved';
const params = { endpoint, redirectUrl, status };
describe('updateStatus', () => { describe('updateStatus', () => {
const endpoint = '123/stacktrace';
const redirectUrl = '/list';
const status = 'resolved';
it('should handle successful status update', done => { it('should handle successful status update', done => {
mock.onPut().reply(200, {}); mock.onPut().reply(200, {});
testAction( testAction(
actions.updateStatus, actions.updateStatus,
{ endpoint, redirectUrl, status }, params,
{}, {},
[ [
{ {
payload: true, payload: 'resolved',
type: types.SET_UPDATING_RESOLVE_STATUS, type: types.SET_ERROR_STATUS,
},
{
payload: false,
type: 'SET_UPDATING_RESOLVE_STATUS',
}, },
], ],
[], [],
...@@ -52,27 +50,29 @@ describe('Sentry common store actions', () => { ...@@ -52,27 +50,29 @@ describe('Sentry common store actions', () => {
it('should handle unsuccessful status update', done => { it('should handle unsuccessful status update', done => {
mock.onPut().reply(400, {}); mock.onPut().reply(400, {});
testAction( testAction(actions.updateStatus, params, {}, [], [], () => {
actions.updateStatus, expect(visitUrl).not.toHaveBeenCalled();
{ endpoint, redirectUrl, status }, expect(createFlash).toHaveBeenCalledTimes(1);
{}, done();
[ });
{
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();
},
);
}); });
}); });
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);
}));
});
}); });
...@@ -12,6 +12,7 @@ describe GitlabSchema.types['SentryErrorCollection'] do ...@@ -12,6 +12,7 @@ describe GitlabSchema.types['SentryErrorCollection'] do
errors errors
detailed_error detailed_error
external_url external_url
error_stack_trace
] ]
is_expected.to have_graphql_fields(*expected_fields) is_expected.to have_graphql_fields(*expected_fields)
......
# 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
# 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
...@@ -83,7 +83,6 @@ describe Projects::ErrorTrackingHelper do ...@@ -83,7 +83,6 @@ describe Projects::ErrorTrackingHelper do
describe '#error_details_data' do describe '#error_details_data' do
let(:issue_id) { 1234 } let(:issue_id) { 1234 }
let(:route_params) { [project.owner, project, issue_id, { format: :json }] } 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(:details_path) { details_namespace_project_error_tracking_index_path(*route_params) }
let(:project_path) { project.full_path } let(:project_path) { project.full_path }
let(:stack_trace_path) { stack_trace_namespace_project_error_tracking_index_path(*route_params) } let(:stack_trace_path) { stack_trace_namespace_project_error_tracking_index_path(*route_params) }
...@@ -91,10 +90,6 @@ describe Projects::ErrorTrackingHelper do ...@@ -91,10 +90,6 @@ describe Projects::ErrorTrackingHelper do
let(:result) { helper.error_details_data(project, issue_id) } 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 it 'returns the correct issue id' do
expect(result['issue-id']).to eq issue_id expect(result['issue-id']).to eq issue_id
end end
......
...@@ -4,9 +4,10 @@ require 'spec_helper' ...@@ -4,9 +4,10 @@ require 'spec_helper'
describe Gitlab::MailRoom do describe Gitlab::MailRoom do
let(:default_port) { 143 } let(:default_port) { 143 }
let(:default_config) do let(:yml_config) do
{ {
enabled: false, enabled: true,
address: 'address@example.com',
port: default_port, port: default_port,
ssl: false, ssl: false,
start_tls: false, start_tls: false,
...@@ -16,71 +17,73 @@ describe Gitlab::MailRoom do ...@@ -16,71 +17,73 @@ describe Gitlab::MailRoom do
} }
end end
shared_examples_for 'only truthy if both enabled and address are truthy' do |target_proc| let(:custom_config) { {} }
context 'with both enabled and address as truthy values' do let(:incoming_email_config) { yml_config.merge(custom_config) }
it 'is truthy' do let(:service_desk_email_config) { yml_config.merge(custom_config) }
stub_config(enabled: true, address: 'localhost')
expect(target_proc.call).to be_truthy let(:configs) do
end {
end incoming_email: incoming_email_config,
service_desk_email: service_desk_email_config
context 'with address only as truthy' do }
it 'is falsey' do end
stub_config(enabled: false, address: 'localhost')
expect(target_proc.call).to be_falsey
end
end
context 'with enabled only as truthy' do before do
it 'is falsey' do described_class.instance_variable_set(:@enabled_configs, nil)
stub_config(enabled: true, address: nil) end
expect(target_proc.call).to be_falsey describe '#enabled_configs' do
end before do
allow(described_class).to receive(:load_yaml).and_return(configs)
end end
context 'with neither address nor enabled as truthy' do context 'when both email and address is set' do
it 'is falsey' do it 'returns email configs' do
stub_config(enabled: false, address: nil) expect(described_class.enabled_configs.size).to eq(2)
expect(target_proc.call).to be_falsey
end end
end end
end
before do
described_class.reset_config!
allow(File).to receive(:exist?).and_return true
end
describe '#config' do context 'when the yml file cannot be found' do
context 'if the yml file cannot be found' do
before do before do
allow(File).to receive(:exist?).and_return false allow(described_class).to receive(:config_file).and_return('not_existing_file')
end end
it 'returns an empty hash' do it 'returns an empty list' do
expect(described_class.config).to be_empty expect(described_class.enabled_configs).to be_empty
end end
end end
before do context 'when email is disabled' do
allow(described_class).to receive(:load_from_yaml).and_return(default_config) let(:custom_config) { { enabled: false } }
it 'returns an empty list' do
expect(described_class.enabled_configs).to be_empty
end
end end
it 'sets up config properly' do context 'when email is enabled but address is not set' do
expected_result = default_config 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 end
context 'when a config value is missing from the yml file' do 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 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
end end
...@@ -91,50 +94,31 @@ describe Gitlab::MailRoom do ...@@ -91,50 +94,31 @@ describe Gitlab::MailRoom do
allow(Gitlab::Redis::Queues).to receive(:new).and_return(fake_redis_queues) allow(Gitlab::Redis::Queues).to receive(:new).and_return(fake_redis_queues)
end 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 end
describe 'setting up the log path' do describe 'setting up the log path' do
context 'if the log path is a relative path' do context 'if the log path is a relative path' do
it 'expands the log path to an absolute value' do let(:custom_config) { { log_path: 'tiny_log.log' } }
stub_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 expect(new_path.absolute?).to be_truthy
end end
end end
context 'if the log path is absolute path' do context 'if the log path is absolute path' do
it 'leaves the path as-is' do let(:custom_config) { { log_path: '/dev/null' } }
new_path = '/dev/null'
stub_config(log_path: new_path)
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
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 end
...@@ -40,8 +40,8 @@ describe 'sentry errors requests' do ...@@ -40,8 +40,8 @@ describe 'sentry errors requests' do
post_graphql(query, current_user: current_user) post_graphql(query, current_user: current_user)
end end
it "is expected to return an empty error" do it 'is expected to return an empty error' do
expect(error_data).to eq nil expect(error_data).to be_nil
end end
end end
...@@ -49,7 +49,7 @@ describe 'sentry errors requests' do ...@@ -49,7 +49,7 @@ describe 'sentry errors requests' do
before do before do
allow_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting) allow_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting)
.to receive(:issue_details) .to receive(:issue_details)
.and_return({ issue: sentry_detailed_error }) .and_return(issue: sentry_detailed_error)
post_graphql(query, current_user: current_user) post_graphql(query, current_user: current_user)
end end
...@@ -72,8 +72,8 @@ describe 'sentry errors requests' do ...@@ -72,8 +72,8 @@ describe 'sentry errors requests' do
context 'user does not have permission' do context 'user does not have permission' do
let(:current_user) { create(:user) } let(:current_user) { create(:user) }
it "is expected to return an empty error" do it 'is expected to return an empty error' do
expect(error_data).to eq nil expect(error_data).to be_nil
end end
end end
end end
...@@ -82,13 +82,13 @@ describe 'sentry errors requests' do ...@@ -82,13 +82,13 @@ describe 'sentry errors requests' do
before do before do
expect_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting) expect_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting)
.to receive(:issue_details) .to receive(:issue_details)
.and_return({ error: 'error message' }) .and_return(error: 'error message')
post_graphql(query, current_user: current_user) post_graphql(query, current_user: current_user)
end end
it 'is expected to handle the error and return nil' do 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 end
end end
...@@ -132,8 +132,8 @@ describe 'sentry errors requests' do ...@@ -132,8 +132,8 @@ describe 'sentry errors requests' do
post_graphql(query, current_user: current_user) post_graphql(query, current_user: current_user)
end end
it "is expected to return nil" do it 'is expected to return nil' do
expect(error_data).to eq nil expect(error_data).to be_nil
end end
end end
...@@ -141,7 +141,7 @@ describe 'sentry errors requests' do ...@@ -141,7 +141,7 @@ describe 'sentry errors requests' do
before do before do
expect_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting) expect_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting)
.to receive(:list_sentry_issues) .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) post_graphql(query, current_user: current_user)
end end
...@@ -174,17 +174,82 @@ describe 'sentry errors requests' do ...@@ -174,17 +174,82 @@ describe 'sentry errors requests' do
end end
end end
context "sentry api itself errors out" do context 'sentry api itself errors out' do
before do before do
expect_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting) expect_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting)
.to receive(:list_sentry_issues) .to receive(:list_sentry_issues)
.and_return({ error: 'error message' }) .and_return(error: 'error message')
post_graphql(query, current_user: current_user) post_graphql(query, current_user: current_user)
end end
it 'is expected to handle the error and return nil' do 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 end
end end
......
...@@ -3,11 +3,34 @@ ...@@ -3,11 +3,34 @@
RSpec.shared_examples 'setting sentry error data' do RSpec.shared_examples 'setting sentry error data' do
it 'sets the sentry error data correctly' do it 'sets the sentry error data correctly' do
aggregate_failures 'testing the sentry error is correct' do aggregate_failures 'testing the sentry error is correct' do
expect(error['id']).to eql sentry_error.to_global_id.to_s expect(error['id']).to eq sentry_error.to_global_id.to_s
expect(error['sentryId']).to eql sentry_error.id.to_s expect(error['sentryId']).to eq sentry_error.id.to_s
expect(error['status']).to eql sentry_error.status.upcase expect(error['status']).to eq sentry_error.status.upcase
expect(error['firstSeen']).to eql sentry_error.first_seen expect(error['firstSeen']).to eq sentry_error.first_seen
expect(error['lastSeen']).to eql sentry_error.last_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 end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment