Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
0434f38ef1
commit
d5d3c03598
51 changed files with 1145 additions and 313 deletions
|
@ -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)
|
||||
|
|
|
@ -13,3 +13,9 @@ export const severityLevelVariant = {
|
|||
[severityLevel.INFO]: 'info',
|
||||
[severityLevel.DEBUG]: 'light',
|
||||
};
|
||||
|
||||
export const errorStatus = {
|
||||
IGNORED: 'ignored',
|
||||
RESOLVED: 'resolved',
|
||||
UNRESOLVED: 'unresolved',
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -6,6 +6,7 @@ query errorDetails($fullPath: ID!, $errorId: ID!) {
|
|||
title
|
||||
userCount
|
||||
count
|
||||
status
|
||||
firstSeen
|
||||
lastSeen
|
||||
message
|
||||
|
|
|
@ -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 () => {};
|
||||
|
|
|
@ -5,4 +5,5 @@ export default () => ({
|
|||
loadingStacktrace: true,
|
||||
updatingResolveStatus: false,
|
||||
updatingIgnoreStatus: false,
|
||||
errorStatus: '',
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Make Explore Projects default to All
|
||||
merge_request: 23811
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Reverse actions for resolve/ignore Sentry issue
|
||||
merge_request: 23516
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add Sentry error stack trace to GraphQL API
|
||||
merge_request: 23750
|
||||
author:
|
||||
type: added
|
5
changelogs/unreleased/refactoring-entities-file-8.yml
Normal file
5
changelogs/unreleased/refactoring-entities-file-8.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Separate snippet entities into own class files
|
||||
merge_request: 24183
|
||||
author: Rajendra Kadam
|
||||
type: added
|
|
@ -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| %>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -224,6 +224,8 @@
|
|||
- 2
|
||||
- - self_monitoring_project_delete
|
||||
- 2
|
||||
- - service_desk_email_receiver
|
||||
- 1
|
||||
- - system_hook_push
|
||||
- 1
|
||||
- - todos_destroyer
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
|
9
lib/api/entities/basic_ref.rb
Normal file
9
lib/api/entities/basic_ref.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class BasicRef < Grape::Entity
|
||||
expose :type, :name
|
||||
end
|
||||
end
|
||||
end
|
41
lib/api/entities/branch.rb
Normal file
41
lib/api/entities/branch.rb
Normal file
|
@ -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
|
11
lib/api/entities/personal_snippet.rb
Normal file
11
lib/api/entities/personal_snippet.rb
Normal file
|
@ -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
|
8
lib/api/entities/project_snippet.rb
Normal file
8
lib/api/entities/project_snippet.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
# frozen_String_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class ProjectSnippet < Entities::Snippet
|
||||
end
|
||||
end
|
||||
end
|
15
lib/api/entities/snippet.rb
Normal file
15
lib/api/entities/snippet.rb
Normal file
|
@ -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
|
15
lib/api/entities/tree_object.rb
Normal file
15
lib/api/entities/tree_object.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
end
|
||||
|
||||
def config
|
||||
@config ||= fetch_config
|
||||
end
|
||||
|
||||
def reset_config!
|
||||
@config = nil
|
||||
def enabled_configs
|
||||
@enabled_configs ||= configs.select { |config| enabled?(config) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_config
|
||||
def enabled?(config)
|
||||
config[:enabled] && !config[:address].to_s.empty?
|
||||
end
|
||||
|
||||
def configs
|
||||
ADDRESS_SPECIFIC_CONFIG.keys.map { |key| fetch_config(key) }
|
||||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
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(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)
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
11
spec/fixtures/config/mail_room_disabled.yml
vendored
11
spec/fixtures/config/mail_room_disabled.yml
vendored
|
@ -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"
|
||||
|
|
11
spec/fixtures/config/mail_room_enabled.yml
vendored
11
spec/fixtures/config/mail_room_enabled.yml
vendored
|
@ -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"
|
||||
|
|
|
@ -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}"]`);
|
||||
|
|
|
@ -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);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
context 'with enabled only as truthy' do
|
||||
it 'is falsey' do
|
||||
stub_config(enabled: true, address: nil)
|
||||
|
||||
expect(target_proc.call).to be_falsey
|
||||
end
|
||||
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
|
||||
end
|
||||
end
|
||||
let(:configs) do
|
||||
{
|
||||
incoming_email: incoming_email_config,
|
||||
service_desk_email: service_desk_email_config
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
described_class.reset_config!
|
||||
allow(File).to receive(:exist?).and_return true
|
||||
described_class.instance_variable_set(:@enabled_configs, nil)
|
||||
end
|
||||
|
||||
describe '#config' do
|
||||
context 'if the yml file cannot be found' do
|
||||
before do
|
||||
allow(File).to receive(:exist?).and_return false
|
||||
end
|
||||
|
||||
it 'returns an empty hash' do
|
||||
expect(described_class.config).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '#enabled_configs' do
|
||||
before do
|
||||
allow(described_class).to receive(:load_from_yaml).and_return(default_config)
|
||||
allow(described_class).to receive(:load_yaml).and_return(configs)
|
||||
end
|
||||
|
||||
it 'sets up config properly' do
|
||||
expected_result = default_config
|
||||
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
|
||||
|
||||
expect(described_class.config).to match expected_result
|
||||
context 'when the yml file cannot be found' do
|
||||
before do
|
||||
allow(described_class).to receive(:config_file).and_return('not_existing_file')
|
||||
end
|
||||
|
||||
it 'returns an empty list' do
|
||||
expect(described_class.enabled_configs).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
context 'when email is enabled but address is not set' do
|
||||
let(:custom_config) { { enabled: true, address: '' } }
|
||||
|
||||
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
|
||||
it 'overwrites missing values with the default' do
|
||||
stub_config(port: nil)
|
||||
let(:yml_config) { {} }
|
||||
let(:custom_config) { { enabled: true, address: 'address@example.com' } }
|
||||
|
||||
expect(described_class.config[:port]).to eq default_port
|
||||
it 'overwrites missing values with the default' do
|
||||
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 } }
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue