Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-01-31 21:08:52 +00:00
parent 0434f38ef1
commit d5d3c03598
51 changed files with 1145 additions and 313 deletions

View file

@ -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)

View file

@ -13,3 +13,9 @@ export const severityLevelVariant = {
[severityLevel.INFO]: 'info',
[severityLevel.DEBUG]: 'light',
};
export const errorStatus = {
IGNORED: 'ignored',
RESOLVED: 'resolved',
UNRESOLVED: 'unresolved',
};

View file

@ -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"

View file

@ -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,

View file

@ -6,6 +6,7 @@ query errorDetails($fullPath: ID!, $errorId: ID!) {
title
userCount
count
status
firstSeen
lastSeen
message

View file

@ -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 () => {};

View file

@ -5,4 +5,5 @@ export default () => ({
loadingStacktrace: true,
updatingResolveStatus: false,
updatingIgnoreStatus: false,
errorStatus: '',
});

View file

@ -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';

View file

@ -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;
},
};

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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),

View file

@ -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

View file

@ -0,0 +1,5 @@
---
title: Make Explore Projects default to All
merge_request: 23811
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Reverse actions for resolve/ignore Sentry issue
merge_request: 23516
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Add Sentry error stack trace to GraphQL API
merge_request: 23750
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Separate snippet entities into own class files
merge_request: 24183
author: Rajendra Kadam
type: added

View file

@ -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| %>

View file

@ -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

View file

@ -224,6 +224,8 @@
- 2
- - self_monitoring_project_delete
- 2
- - service_desk_email_receiver
- 1
- - system_hook_push
- 1
- - todos_destroyer

View file

@ -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

View file

@ -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
"""

View file

@ -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",

View file

@ -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

View file

@ -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) }

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
module API
module Entities
class BasicRef < Grape::Entity
expose :type, :name
end
end
end

View 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

View 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

View file

@ -0,0 +1,8 @@
# frozen_String_literal: true
module API
module Entities
class ProjectSnippet < Entities::Snippet
end
end
end

View 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

View 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

View file

@ -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

View file

@ -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

View file

@ -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 ""

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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}"]`);

View file

@ -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);
}));
});
});

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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