Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
46b10c0fc8
commit
7cc6872401
File diff suppressed because it is too large
Load Diff
|
@ -10,6 +10,7 @@ import {
|
|||
GlLoadingIcon,
|
||||
} from '@gitlab/ui';
|
||||
import { s__, __, sprintf } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
import {
|
||||
NAME_REGEX_LENGTH,
|
||||
UPDATE_SETTINGS_ERROR_MESSAGE,
|
||||
|
@ -27,10 +28,18 @@ export default {
|
|||
GlCard,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
labelsConfig: {
|
||||
cols: 3,
|
||||
align: 'right',
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tracking: {
|
||||
label: 'docker_container_retention_and_expiration_policies',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['formOptions', 'isLoading']),
|
||||
...mapComputed(
|
||||
|
@ -86,7 +95,12 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
...mapActions(['resetSettings', 'saveSettings']),
|
||||
reset() {
|
||||
this.track('reset_form');
|
||||
this.resetSettings();
|
||||
},
|
||||
submit() {
|
||||
this.track('submit_form');
|
||||
this.saveSettings()
|
||||
.then(() => this.$toast.show(UPDATE_SETTINGS_SUCCESS_MESSAGE, { type: 'success' }))
|
||||
.catch(() => this.$toast.show(UPDATE_SETTINGS_ERROR_MESSAGE, { type: 'error' }));
|
||||
|
@ -96,7 +110,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<form ref="form-element" @submit.prevent="submit" @reset.prevent="resetSettings">
|
||||
<form ref="form-element" @submit.prevent="submit" @reset.prevent="reset">
|
||||
<gl-card>
|
||||
<template #header>
|
||||
{{ s__('ContainerRegistry|Tag expiration policy') }}
|
||||
|
|
|
@ -3,7 +3,13 @@
|
|||
class Admin::ApplicationSettingsController < Admin::ApplicationController
|
||||
include InternalRedirect
|
||||
|
||||
# NOTE: Use @application_setting in this controller when you need to access
|
||||
# application_settings after it has been modified. This is because the
|
||||
# ApplicationSetting model uses Gitlab::ThreadMemoryCache for caching and the
|
||||
# cache might be stale immediately after an update.
|
||||
# https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/30233
|
||||
before_action :set_application_setting
|
||||
|
||||
before_action :whitelist_query_limiting, only: [:usage_data]
|
||||
before_action :validate_self_monitoring_feature_flag_enabled, only: [
|
||||
:create_self_monitoring_project,
|
||||
|
@ -79,6 +85,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
redirect_to ::Gitlab::LetsEncrypt.terms_of_service_url
|
||||
end
|
||||
|
||||
# Specs are in spec/requests/self_monitoring_project_spec.rb
|
||||
def create_self_monitoring_project
|
||||
job_id = SelfMonitoringProjectCreateWorker.perform_async
|
||||
|
||||
|
@ -88,6 +95,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
}
|
||||
end
|
||||
|
||||
# Specs are in spec/requests/self_monitoring_project_spec.rb
|
||||
def status_create_self_monitoring_project
|
||||
job_id = params[:job_id].to_s
|
||||
|
||||
|
@ -98,10 +106,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
}
|
||||
end
|
||||
|
||||
if Gitlab::CurrentSettings.self_monitoring_project_id.present?
|
||||
return render status: :ok, json: self_monitoring_data
|
||||
|
||||
elsif SelfMonitoringProjectCreateWorker.in_progress?(job_id)
|
||||
if SelfMonitoringProjectCreateWorker.in_progress?(job_id)
|
||||
::Gitlab::PollingInterval.set_header(response, interval: 3_000)
|
||||
|
||||
return render status: :accepted, json: {
|
||||
|
@ -109,12 +114,17 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
}
|
||||
end
|
||||
|
||||
if @application_setting.self_monitoring_project_id.present?
|
||||
return render status: :ok, json: self_monitoring_data
|
||||
end
|
||||
|
||||
render status: :bad_request, json: {
|
||||
message: _('Self-monitoring project does not exist. Please check logs ' \
|
||||
'for any error messages')
|
||||
}
|
||||
end
|
||||
|
||||
# Specs are in spec/requests/self_monitoring_project_spec.rb
|
||||
def delete_self_monitoring_project
|
||||
job_id = SelfMonitoringProjectDeleteWorker.perform_async
|
||||
|
||||
|
@ -124,6 +134,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
}
|
||||
end
|
||||
|
||||
# Specs are in spec/requests/self_monitoring_project_spec.rb
|
||||
def status_delete_self_monitoring_project
|
||||
job_id = params[:job_id].to_s
|
||||
|
||||
|
@ -134,12 +145,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
}
|
||||
end
|
||||
|
||||
if Gitlab::CurrentSettings.self_monitoring_project_id.nil?
|
||||
return render status: :ok, json: {
|
||||
message: _('Self-monitoring project has been successfully deleted')
|
||||
}
|
||||
|
||||
elsif SelfMonitoringProjectDeleteWorker.in_progress?(job_id)
|
||||
if SelfMonitoringProjectDeleteWorker.in_progress?(job_id)
|
||||
::Gitlab::PollingInterval.set_header(response, interval: 3_000)
|
||||
|
||||
return render status: :accepted, json: {
|
||||
|
@ -147,6 +153,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
}
|
||||
end
|
||||
|
||||
if @application_setting.self_monitoring_project_id.nil?
|
||||
return render status: :ok, json: {
|
||||
message: _('Self-monitoring project has been successfully deleted')
|
||||
}
|
||||
end
|
||||
|
||||
render status: :bad_request, json: {
|
||||
message: _('Self-monitoring project was not deleted. Please check logs ' \
|
||||
'for any error messages')
|
||||
|
@ -161,8 +173,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
|
||||
def self_monitoring_data
|
||||
{
|
||||
project_id: Gitlab::CurrentSettings.self_monitoring_project_id,
|
||||
project_full_path: Gitlab::CurrentSettings.self_monitoring_project&.full_path
|
||||
project_id: @application_setting.self_monitoring_project_id,
|
||||
project_full_path: @application_setting.self_monitoring_project&.full_path
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ class Admin::SpamLogsController < Admin::ApplicationController
|
|||
def mark_as_ham
|
||||
spam_log = SpamLog.find(params[:id])
|
||||
|
||||
if HamService.new(spam_log).mark_as_ham!
|
||||
if Spam::HamService.new(spam_log).mark_as_ham!
|
||||
redirect_to admin_spam_logs_path, notice: _('Spam log successfully submitted as ham.')
|
||||
else
|
||||
redirect_to admin_spam_logs_path, alert: _('Error with Akismet. Please check the logs for more info.')
|
||||
|
|
|
@ -8,7 +8,6 @@ module Resolvers
|
|||
description: 'ID of the Sentry issue'
|
||||
|
||||
def resolve(**args)
|
||||
project = object
|
||||
current_user = context[:current_user]
|
||||
issue_id = GlobalID.parse(args[:id]).model_id
|
||||
|
||||
|
@ -23,6 +22,14 @@ module Resolvers
|
|||
|
||||
issue
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def project
|
||||
return object.gitlab_project if object.respond_to?(:gitlab_project)
|
||||
|
||||
object
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Resolvers
|
||||
module ErrorTracking
|
||||
class SentryErrorCollectionResolver < BaseResolver
|
||||
def resolve(**args)
|
||||
project = object
|
||||
|
||||
service = ::ErrorTracking::ListIssuesService.new(
|
||||
project,
|
||||
context[:current_user]
|
||||
)
|
||||
|
||||
Gitlab::ErrorTracking::ErrorCollection.new(
|
||||
external_url: service.external_url,
|
||||
project: project
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Resolvers
|
||||
module ErrorTracking
|
||||
class SentryErrorsResolver < BaseResolver
|
||||
def resolve(**args)
|
||||
args[:cursor] = args.delete(:after)
|
||||
project = object.project
|
||||
|
||||
result = ::ErrorTracking::ListIssuesService.new(
|
||||
project,
|
||||
context[:current_user],
|
||||
args
|
||||
).execute
|
||||
|
||||
next_cursor = result[:pagination]&.dig('next', 'cursor')
|
||||
previous_cursor = result[:pagination]&.dig('previous', 'cursor')
|
||||
issues = result[:issues]
|
||||
|
||||
# ReactiveCache is still fetching data
|
||||
return if issues.nil?
|
||||
|
||||
Gitlab::Graphql::ExternallyPaginatedArray.new(previous_cursor, next_cursor, *issues)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,8 +4,9 @@ module Types
|
|||
module ErrorTracking
|
||||
class SentryDetailedErrorType < ::Types::BaseObject
|
||||
graphql_name 'SentryDetailedError'
|
||||
description 'A Sentry error.'
|
||||
|
||||
present_using SentryDetailedErrorPresenter
|
||||
present_using SentryErrorPresenter
|
||||
|
||||
authorize :read_sentry_issue
|
||||
|
||||
|
@ -92,18 +93,6 @@ module Types
|
|||
field :tags, Types::ErrorTracking::SentryErrorTagsType,
|
||||
null: false,
|
||||
description: 'Tags associated with the Sentry Error'
|
||||
|
||||
def first_seen
|
||||
DateTime.parse(object.first_seen)
|
||||
end
|
||||
|
||||
def last_seen
|
||||
DateTime.parse(object.last_seen)
|
||||
end
|
||||
|
||||
def project_id
|
||||
Gitlab::GlobalId.build(model_name: 'Project', id: object.project_id).to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module ErrorTracking
|
||||
class SentryErrorCollectionType < ::Types::BaseObject
|
||||
graphql_name 'SentryErrorCollection'
|
||||
description 'An object containing a collection of Sentry errors, and a detailed error.'
|
||||
|
||||
authorize :read_sentry_issue
|
||||
|
||||
field :errors,
|
||||
Types::ErrorTracking::SentryErrorType.connection_type,
|
||||
connection: false,
|
||||
null: true,
|
||||
description: "Collection of Sentry Errors",
|
||||
extensions: [Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension],
|
||||
resolver: Resolvers::ErrorTracking::SentryErrorsResolver do
|
||||
argument :search_term,
|
||||
String,
|
||||
description: 'Search term for the Sentry error.',
|
||||
required: false
|
||||
argument :sort,
|
||||
String,
|
||||
description: 'Attribute to sort on. Options are frequency, first_seen, last_seen. last_seen is default.',
|
||||
required: false
|
||||
end
|
||||
field :detailed_error, Types::ErrorTracking::SentryDetailedErrorType,
|
||||
null: true,
|
||||
description: 'Detailed version of a Sentry error on the project',
|
||||
resolver: Resolvers::ErrorTracking::SentryDetailedErrorResolver
|
||||
field :external_url,
|
||||
GraphQL::STRING_TYPE,
|
||||
null: true,
|
||||
description: "External URL for Sentry"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,70 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module ErrorTracking
|
||||
# rubocop: disable Graphql/AuthorizeTypes
|
||||
class SentryErrorType < ::Types::BaseObject
|
||||
graphql_name 'SentryError'
|
||||
description 'A Sentry error. A simplified version of SentryDetailedError.'
|
||||
|
||||
present_using SentryErrorPresenter
|
||||
|
||||
field :id, GraphQL::ID_TYPE,
|
||||
null: false,
|
||||
description: 'ID (global ID) of the error'
|
||||
field :sentry_id, GraphQL::STRING_TYPE,
|
||||
method: :id,
|
||||
null: false,
|
||||
description: 'ID (Sentry ID) of the error'
|
||||
field :first_seen, Types::TimeType,
|
||||
null: false,
|
||||
description: 'Timestamp when the error was first seen'
|
||||
field :last_seen, Types::TimeType,
|
||||
null: false,
|
||||
description: 'Timestamp when the error was last seen'
|
||||
field :title, GraphQL::STRING_TYPE,
|
||||
null: false,
|
||||
description: 'Title of the error'
|
||||
field :type, GraphQL::STRING_TYPE,
|
||||
null: false,
|
||||
description: 'Type of the error'
|
||||
field :user_count, GraphQL::INT_TYPE,
|
||||
null: false,
|
||||
description: 'Count of users affected by the error'
|
||||
field :count, GraphQL::INT_TYPE,
|
||||
null: false,
|
||||
description: 'Count of occurrences'
|
||||
field :message, GraphQL::STRING_TYPE,
|
||||
null: true,
|
||||
description: 'Sentry metadata message of the error'
|
||||
field :culprit, GraphQL::STRING_TYPE,
|
||||
null: false,
|
||||
description: 'Culprit of the error'
|
||||
field :external_url, GraphQL::STRING_TYPE,
|
||||
null: false,
|
||||
description: 'External URL of the error'
|
||||
field :short_id, GraphQL::STRING_TYPE,
|
||||
null: false,
|
||||
description: 'Short ID (Sentry ID) of the error'
|
||||
field :status, Types::ErrorTracking::SentryErrorStatusEnum,
|
||||
null: false,
|
||||
description: 'Status of the error'
|
||||
field :frequency, [Types::ErrorTracking::SentryErrorFrequencyType],
|
||||
null: false,
|
||||
description: 'Last 24hr stats of the error'
|
||||
field :sentry_project_id, GraphQL::ID_TYPE,
|
||||
method: :project_id,
|
||||
null: false,
|
||||
description: 'ID of the project (Sentry project)'
|
||||
field :sentry_project_name, GraphQL::STRING_TYPE,
|
||||
method: :project_name,
|
||||
null: false,
|
||||
description: 'Name of the project affected by the error'
|
||||
field :sentry_project_slug, GraphQL::STRING_TYPE,
|
||||
method: :project_slug,
|
||||
null: false,
|
||||
description: 'Slug of the project affected by the error'
|
||||
end
|
||||
# rubocop: enable Graphql/AuthorizeTypes
|
||||
end
|
||||
end
|
|
@ -173,6 +173,12 @@ module Types
|
|||
null: true,
|
||||
description: 'Snippets of the project',
|
||||
resolver: Resolvers::Projects::SnippetsResolver
|
||||
|
||||
field :sentry_errors,
|
||||
Types::ErrorTracking::SentryErrorCollectionType,
|
||||
null: true,
|
||||
description: 'Paginated collection of Sentry errors on the project',
|
||||
resolver: Resolvers::ErrorTracking::SentryErrorCollectionResolver
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -484,10 +484,10 @@ class Commit
|
|||
end
|
||||
|
||||
def commit_reference(from, referable_commit_id, full: false)
|
||||
reference = project.to_reference(from, full: full)
|
||||
base = project.to_reference_base(from, full: full)
|
||||
|
||||
if reference.present?
|
||||
"#{reference}#{self.class.reference_prefix}#{referable_commit_id}"
|
||||
if base.present?
|
||||
"#{base}#{self.class.reference_prefix}#{referable_commit_id}"
|
||||
else
|
||||
referable_commit_id
|
||||
end
|
||||
|
|
|
@ -92,7 +92,7 @@ class CommitRange
|
|||
alias_method :id, :to_s
|
||||
|
||||
def to_reference(from = nil, full: false)
|
||||
project_reference = project.to_reference(from, full: full)
|
||||
project_reference = project.to_reference_base(from, full: full)
|
||||
|
||||
if project_reference.present?
|
||||
project_reference + self.class.reference_prefix + self.id
|
||||
|
@ -102,7 +102,7 @@ class CommitRange
|
|||
end
|
||||
|
||||
def reference_link_text(from = nil)
|
||||
project_reference = project.to_reference(from)
|
||||
project_reference = project.to_reference_base(from)
|
||||
reference = ref_from + notation + ref_to
|
||||
|
||||
if project_reference.present?
|
||||
|
|
|
@ -23,6 +23,14 @@ module Referable
|
|||
''
|
||||
end
|
||||
|
||||
# If this referable object can serve as the base for the
|
||||
# reference of child objects (e.g. projects are the base of
|
||||
# issues), but it is formatted differently, then you may wish
|
||||
# to override this method.
|
||||
def to_reference_base(from = nil, full:)
|
||||
to_reference(from, full: full)
|
||||
end
|
||||
|
||||
def reference_link_text(from = nil)
|
||||
to_reference(from)
|
||||
end
|
||||
|
|
|
@ -173,7 +173,7 @@ class Issue < ApplicationRecord
|
|||
def to_reference(from = nil, full: false)
|
||||
reference = "#{self.class.reference_prefix}#{iid}"
|
||||
|
||||
"#{project.to_reference(from, full: full)}#{reference}"
|
||||
"#{project.to_reference_base(from, full: full)}#{reference}"
|
||||
end
|
||||
|
||||
def suggested_branch_name
|
||||
|
|
|
@ -225,7 +225,7 @@ class Label < ApplicationRecord
|
|||
reference = "#{self.class.reference_prefix}#{format_reference}"
|
||||
|
||||
if from
|
||||
"#{from.to_reference(target_project, full: full)}#{reference}"
|
||||
"#{from.to_reference_base(target_project, full: full)}#{reference}"
|
||||
else
|
||||
reference
|
||||
end
|
||||
|
|
|
@ -396,7 +396,7 @@ class MergeRequest < ApplicationRecord
|
|||
def to_reference(from = nil, full: false)
|
||||
reference = "#{self.class.reference_prefix}#{iid}"
|
||||
|
||||
"#{project.to_reference(from, full: full)}#{reference}"
|
||||
"#{project.to_reference_base(from, full: full)}#{reference}"
|
||||
end
|
||||
|
||||
def commits(limit: nil)
|
||||
|
|
|
@ -228,7 +228,7 @@ class Milestone < ApplicationRecord
|
|||
reference = "#{self.class.reference_prefix}#{format_reference}"
|
||||
|
||||
if project
|
||||
"#{project.to_reference(from, full: full)}#{reference}"
|
||||
"#{project.to_reference_base(from, full: full)}#{reference}"
|
||||
else
|
||||
reference
|
||||
end
|
||||
|
|
|
@ -1068,12 +1068,19 @@ class Project < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def to_reference_with_postfix
|
||||
"#{to_reference(full: true)}#{self.class.reference_postfix}"
|
||||
# Produce a valid reference (see Referable#to_reference)
|
||||
#
|
||||
# NB: For projects, all references are 'full' - i.e. they all include the
|
||||
# full_path, rather than just the project name. For this reason, we ignore
|
||||
# the value of `full:` passed to this method, which is part of the Referable
|
||||
# interface.
|
||||
def to_reference(from = nil, full: false)
|
||||
base = to_reference_base(from, full: true)
|
||||
"#{base}#{self.class.reference_postfix}"
|
||||
end
|
||||
|
||||
# `from` argument can be a Namespace or Project.
|
||||
def to_reference(from = nil, full: false)
|
||||
def to_reference_base(from = nil, full: false)
|
||||
if full || cross_namespace_reference?(from)
|
||||
full_path
|
||||
elsif cross_project_reference?(from)
|
||||
|
|
|
@ -180,7 +180,7 @@ class Snippet < ApplicationRecord
|
|||
reference = "#{self.class.reference_prefix}#{id}"
|
||||
|
||||
if project.present?
|
||||
"#{project.to_reference(from, full: full)}#{reference}"
|
||||
"#{project.to_reference_base(from, full: full)}#{reference}"
|
||||
else
|
||||
reference
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ErrorTracking
|
||||
class DetailedErrorPolicy < BasePolicy
|
||||
class BasePolicy < ::BasePolicy
|
||||
delegate { @subject.gitlab_project }
|
||||
end
|
||||
end
|
|
@ -1,10 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SentryDetailedErrorPresenter < Gitlab::View::Presenter::Delegated
|
||||
class SentryErrorPresenter < Gitlab::View::Presenter::Delegated
|
||||
presents :error
|
||||
|
||||
FrequencyStruct = Struct.new(:time, :count, keyword_init: true)
|
||||
|
||||
def first_seen
|
||||
DateTime.parse(error.first_seen)
|
||||
end
|
||||
|
||||
def last_seen
|
||||
DateTime.parse(error.last_seen)
|
||||
end
|
||||
|
||||
def project_id
|
||||
Gitlab::GlobalId.build(model_name: 'Project', id: error.project_id).to_s
|
||||
end
|
||||
|
||||
def frequency
|
||||
utc_offset = Time.zone_offset('UTC')
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class HamService
|
||||
attr_accessor :spam_log
|
||||
|
||||
def initialize(spam_log)
|
||||
@spam_log = spam_log
|
||||
end
|
||||
|
||||
def mark_as_ham!
|
||||
if akismet.submit_ham
|
||||
spam_log.update_attribute(:submitted_as_ham, true)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def akismet
|
||||
user = spam_log.user
|
||||
@akismet ||= AkismetService.new(
|
||||
user.name,
|
||||
user.email,
|
||||
spam_log.text,
|
||||
ip_address: spam_log.source_ip,
|
||||
user_agent: spam_log.user_agent
|
||||
)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Spam
|
||||
class HamService
|
||||
attr_accessor :spam_log
|
||||
|
||||
def initialize(spam_log)
|
||||
@spam_log = spam_log
|
||||
end
|
||||
|
||||
def mark_as_ham!
|
||||
if akismet.submit_ham
|
||||
spam_log.update_attribute(:submitted_as_ham, true)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def akismet
|
||||
user = spam_log.user
|
||||
@akismet ||= AkismetService.new(
|
||||
user.name,
|
||||
user.email,
|
||||
spam_log.text,
|
||||
ip_address: spam_log.source_ip,
|
||||
user_agent: spam_log.user_agent
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add querying of Sentry errors to Graphql
|
||||
merge_request: 21802
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: refactoring gl_dropdown.js to use ES6 classes instead of constructor functions
|
||||
merge_request: 20488
|
||||
author: nuwe1
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add license FAQ link to license expired message
|
||||
merge_request:
|
||||
author:
|
||||
type: added
|
|
@ -342,16 +342,28 @@ pages:
|
|||
|
||||
1. [Reconfigure GitLab][reconfigure] for the changes to take effect.
|
||||
|
||||
### Using a custom Certificate Authority (CA) with Access Control
|
||||
### Using a custom Certificate Authority (CA)
|
||||
|
||||
When using certificates issued by a custom CA, Access Control on GitLab Pages may fail to work if the custom CA is not recognized.
|
||||
When using certificates issued by a custom CA, [Access Control](../../user/project/pages/pages_access_control.md#gitlab-pages-access-control) and
|
||||
the [online view of HTML job artifacts](../../user/project/pipelines/job_artifacts.md#browsing-artifacts)
|
||||
will fail to work if the custom CA is not recognized.
|
||||
|
||||
This usually results in this error:
|
||||
`Post /oauth/token: x509: certificate signed by unknown authority`.
|
||||
|
||||
For GitLab Pages Access Control with TLS/SSL certs issued by an internal or custom CA:
|
||||
For installation from source this can be fixed by installing the custom Certificate
|
||||
Authority (CA) in the system certificate store.
|
||||
|
||||
1. Copy the certificate bundle to `/opt/gitlab/embedded/ssl/certs/` in `.pem` format.
|
||||
For Omnibus, normally this would be fixed by [installing a custom CA in GitLab Omnibus](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates)
|
||||
but a [bug](https://gitlab.com/gitlab-org/gitlab/issues/25411) is currently preventing
|
||||
that method from working. Use the following workaround:
|
||||
|
||||
1. Append your GitLab server TLS/SSL certficate to `/opt/gitlab/embedded/ssl/certs/cacert.pem` where `gitlab-domain-example.com` is your GitLab application URL
|
||||
|
||||
```bash
|
||||
printf "\ngitlab-domain-example.com\n===========================\n" | sudo tee --append /opt/gitlab/embedded/ssl/certs/cacert.pem
|
||||
echo -n | openssl s_client -connect gitlab-domain-example.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | sudo tee --append /opt/gitlab/embedded/ssl/certs/cacert.pem
|
||||
```
|
||||
|
||||
1. [Restart](../restart_gitlab.md) the GitLab Pages Daemon. For GitLab Omnibus instances:
|
||||
|
||||
|
@ -359,6 +371,9 @@ For GitLab Pages Access Control with TLS/SSL certs issued by an internal or cust
|
|||
sudo gitlab-ctl restart gitlab-pages
|
||||
```
|
||||
|
||||
CAUTION: **Caution:**
|
||||
Some GitLab Omnibus upgrades will revert this workaround and you'll need to apply it again.
|
||||
|
||||
## Activate verbose logging for daemon
|
||||
|
||||
Verbose logging was [introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/2533) in
|
||||
|
|
|
@ -5453,6 +5453,11 @@ type Project {
|
|||
id: ID!
|
||||
): SentryDetailedError
|
||||
|
||||
"""
|
||||
Paginated collection of Sentry errors on the project
|
||||
"""
|
||||
sentryErrors: SentryErrorCollection
|
||||
|
||||
"""
|
||||
E-mail address of the service desk.
|
||||
"""
|
||||
|
@ -6054,6 +6059,9 @@ type RootStorageStatistics {
|
|||
wikiSize: Int!
|
||||
}
|
||||
|
||||
"""
|
||||
A Sentry error.
|
||||
"""
|
||||
type SentryDetailedError {
|
||||
"""
|
||||
Count of occurrences
|
||||
|
@ -6186,6 +6194,186 @@ type SentryDetailedError {
|
|||
userCount: Int!
|
||||
}
|
||||
|
||||
"""
|
||||
A Sentry error. A simplified version of SentryDetailedError.
|
||||
"""
|
||||
type SentryError {
|
||||
"""
|
||||
Count of occurrences
|
||||
"""
|
||||
count: Int!
|
||||
|
||||
"""
|
||||
Culprit of the error
|
||||
"""
|
||||
culprit: String!
|
||||
|
||||
"""
|
||||
External URL of the error
|
||||
"""
|
||||
externalUrl: String!
|
||||
|
||||
"""
|
||||
Timestamp when the error was first seen
|
||||
"""
|
||||
firstSeen: Time!
|
||||
|
||||
"""
|
||||
Last 24hr stats of the error
|
||||
"""
|
||||
frequency: [SentryErrorFrequency!]!
|
||||
|
||||
"""
|
||||
ID (global ID) of the error
|
||||
"""
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
Timestamp when the error was last seen
|
||||
"""
|
||||
lastSeen: Time!
|
||||
|
||||
"""
|
||||
Sentry metadata message of the error
|
||||
"""
|
||||
message: String
|
||||
|
||||
"""
|
||||
ID (Sentry ID) of the error
|
||||
"""
|
||||
sentryId: String!
|
||||
|
||||
"""
|
||||
ID of the project (Sentry project)
|
||||
"""
|
||||
sentryProjectId: ID!
|
||||
|
||||
"""
|
||||
Name of the project affected by the error
|
||||
"""
|
||||
sentryProjectName: String!
|
||||
|
||||
"""
|
||||
Slug of the project affected by the error
|
||||
"""
|
||||
sentryProjectSlug: String!
|
||||
|
||||
"""
|
||||
Short ID (Sentry ID) of the error
|
||||
"""
|
||||
shortId: String!
|
||||
|
||||
"""
|
||||
Status of the error
|
||||
"""
|
||||
status: SentryErrorStatus!
|
||||
|
||||
"""
|
||||
Title of the error
|
||||
"""
|
||||
title: String!
|
||||
|
||||
"""
|
||||
Type of the error
|
||||
"""
|
||||
type: String!
|
||||
|
||||
"""
|
||||
Count of users affected by the error
|
||||
"""
|
||||
userCount: Int!
|
||||
}
|
||||
|
||||
"""
|
||||
An object containing a collection of Sentry errors, and a detailed error.
|
||||
"""
|
||||
type SentryErrorCollection {
|
||||
"""
|
||||
Detailed version of a Sentry error on the project
|
||||
"""
|
||||
detailedError(
|
||||
"""
|
||||
ID of the Sentry issue
|
||||
"""
|
||||
id: ID!
|
||||
): SentryDetailedError
|
||||
|
||||
"""
|
||||
Collection of Sentry Errors
|
||||
"""
|
||||
errors(
|
||||
"""
|
||||
Returns the elements in the list that come after the specified cursor.
|
||||
"""
|
||||
after: String
|
||||
|
||||
"""
|
||||
Returns the elements in the list that come before the specified cursor.
|
||||
"""
|
||||
before: String
|
||||
|
||||
"""
|
||||
Returns the first _n_ elements from the list.
|
||||
"""
|
||||
first: Int
|
||||
|
||||
"""
|
||||
Returns the last _n_ elements from the list.
|
||||
"""
|
||||
last: Int
|
||||
|
||||
"""
|
||||
Search term for the Sentry error.
|
||||
"""
|
||||
searchTerm: String
|
||||
|
||||
"""
|
||||
Attribute to sort on. Options are frequency, first_seen, last_seen. last_seen is default.
|
||||
"""
|
||||
sort: String
|
||||
): SentryErrorConnection
|
||||
|
||||
"""
|
||||
External URL for Sentry
|
||||
"""
|
||||
externalUrl: String
|
||||
}
|
||||
|
||||
"""
|
||||
The connection type for SentryError.
|
||||
"""
|
||||
type SentryErrorConnection {
|
||||
"""
|
||||
A list of edges.
|
||||
"""
|
||||
edges: [SentryErrorEdge]
|
||||
|
||||
"""
|
||||
A list of nodes.
|
||||
"""
|
||||
nodes: [SentryError]
|
||||
|
||||
"""
|
||||
Information to aid in pagination.
|
||||
"""
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
"""
|
||||
An edge in a connection.
|
||||
"""
|
||||
type SentryErrorEdge {
|
||||
"""
|
||||
A cursor for use in pagination.
|
||||
"""
|
||||
cursor: String!
|
||||
|
||||
"""
|
||||
The item at the end of the edge.
|
||||
"""
|
||||
node: SentryError
|
||||
}
|
||||
|
||||
type SentryErrorFrequency {
|
||||
"""
|
||||
Count of errors received since the previously recorded time
|
||||
|
|
|
@ -1433,6 +1433,20 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "sentryErrors",
|
||||
"description": "Paginated collection of Sentry errors on the project",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "SentryErrorCollection",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "serviceDeskAddress",
|
||||
"description": "E-mail address of the service desk.",
|
||||
|
@ -16708,7 +16722,7 @@
|
|||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "SentryDetailedError",
|
||||
"description": null,
|
||||
"description": "A Sentry error.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "count",
|
||||
|
@ -17408,6 +17422,568 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "SentryErrorCollection",
|
||||
"description": "An object containing a collection of Sentry errors, and a detailed error.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "detailedError",
|
||||
"description": "Detailed version of a Sentry error on the project",
|
||||
"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": "SentryDetailedError",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "errors",
|
||||
"description": "Collection of Sentry Errors",
|
||||
"args": [
|
||||
{
|
||||
"name": "after",
|
||||
"description": "Returns the elements in the list that come after the specified cursor.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "before",
|
||||
"description": "Returns the elements in the list that come before the specified cursor.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "first",
|
||||
"description": "Returns the first _n_ elements from the list.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "last",
|
||||
"description": "Returns the last _n_ elements from the list.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "searchTerm",
|
||||
"description": "Search term for the Sentry error.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "sort",
|
||||
"description": "Attribute to sort on. Options are frequency, first_seen, last_seen. last_seen is default.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "SentryErrorConnection",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "externalUrl",
|
||||
"description": "External URL for Sentry",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "SentryErrorConnection",
|
||||
"description": "The connection type for SentryError.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "edges",
|
||||
"description": "A list of edges.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "SentryErrorEdge",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "nodes",
|
||||
"description": "A list of nodes.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "SentryError",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "pageInfo",
|
||||
"description": "Information to aid in pagination.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "PageInfo",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "SentryErrorEdge",
|
||||
"description": "An edge in a connection.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "cursor",
|
||||
"description": "A cursor for use in pagination.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "node",
|
||||
"description": "The item at the end of the edge.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "SentryError",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "SentryError",
|
||||
"description": "A Sentry error. A simplified version of SentryDetailedError.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "count",
|
||||
"description": "Count of occurrences",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "culprit",
|
||||
"description": "Culprit of the error",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "externalUrl",
|
||||
"description": "External URL of the error",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "firstSeen",
|
||||
"description": "Timestamp when the error was first seen",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Time",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "frequency",
|
||||
"description": "Last 24hr stats of the error",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "SentryErrorFrequency",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"description": "ID (global ID) of the error",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "lastSeen",
|
||||
"description": "Timestamp when the error was last seen",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Time",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"description": "Sentry metadata message of the error",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "sentryId",
|
||||
"description": "ID (Sentry ID) of the error",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "sentryProjectId",
|
||||
"description": "ID of the project (Sentry project)",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "sentryProjectName",
|
||||
"description": "Name of the project affected by the error",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "sentryProjectSlug",
|
||||
"description": "Slug of the project affected by the error",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "shortId",
|
||||
"description": "Short ID (Sentry ID) of the error",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"description": "Status of the error",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "SentryErrorStatus",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"description": "Title of the error",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"description": "Type of the error",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "userCount",
|
||||
"description": "Count of users affected by the error",
|
||||
"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",
|
||||
|
|
|
@ -815,6 +815,7 @@ Information about pagination in a connection.
|
|||
| `repository` | Repository | Git repository of the project |
|
||||
| `requestAccessEnabled` | Boolean | Indicates if users can request member access to the project |
|
||||
| `sentryDetailedError` | SentryDetailedError | Detailed version of a Sentry error on the project |
|
||||
| `sentryErrors` | SentryErrorCollection | Paginated collection of Sentry errors on the project |
|
||||
| `serviceDeskAddress` | String | E-mail address of the service desk. |
|
||||
| `serviceDeskEnabled` | Boolean | Indicates if the project has service desk enabled. |
|
||||
| `sharedRunnersEnabled` | Boolean | Indicates if shared runners are enabled on the project |
|
||||
|
@ -919,6 +920,8 @@ Autogenerated return type of RemoveAwardEmoji
|
|||
|
||||
## SentryDetailedError
|
||||
|
||||
A Sentry error.
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `count` | Int! | Count of occurrences |
|
||||
|
@ -948,6 +951,40 @@ Autogenerated return type of RemoveAwardEmoji
|
|||
| `type` | String! | Type of the error |
|
||||
| `userCount` | Int! | Count of users affected by the error |
|
||||
|
||||
## SentryError
|
||||
|
||||
A Sentry error. A simplified version of SentryDetailedError.
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `count` | Int! | Count of occurrences |
|
||||
| `culprit` | String! | Culprit of the error |
|
||||
| `externalUrl` | String! | External URL of the error |
|
||||
| `firstSeen` | Time! | Timestamp when the error was first seen |
|
||||
| `frequency` | SentryErrorFrequency! => Array | Last 24hr stats of the error |
|
||||
| `id` | ID! | ID (global ID) of the error |
|
||||
| `lastSeen` | Time! | Timestamp when the error was last seen |
|
||||
| `message` | String | Sentry metadata message of the error |
|
||||
| `sentryId` | String! | ID (Sentry ID) of the error |
|
||||
| `sentryProjectId` | ID! | ID of the project (Sentry project) |
|
||||
| `sentryProjectName` | String! | Name of the project affected by the error |
|
||||
| `sentryProjectSlug` | String! | Slug of the project affected by the error |
|
||||
| `shortId` | String! | Short ID (Sentry ID) of the error |
|
||||
| `status` | SentryErrorStatus! | Status of the error |
|
||||
| `title` | String! | Title of the error |
|
||||
| `type` | String! | Type of the error |
|
||||
| `userCount` | Int! | Count of users affected by the error |
|
||||
|
||||
## SentryErrorCollection
|
||||
|
||||
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 |
|
||||
| `errors` | SentryErrorConnection | Collection of Sentry Errors |
|
||||
| `externalUrl` | String | External URL for Sentry |
|
||||
|
||||
## SentryErrorFrequency
|
||||
|
||||
| Name | Type | Description |
|
||||
|
|
|
@ -385,6 +385,21 @@ NOTE: **Note:**
|
|||
The usage of `perform_enqueued_jobs` is currently useless since our
|
||||
workers aren't inheriting from `ApplicationJob` / `ActiveJob::Base`.
|
||||
|
||||
#### DNS
|
||||
|
||||
DNS requests are stubbed universally in the test suite
|
||||
(as of [!22368](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22368)), as DNS can
|
||||
cause issues depending on the developer's local network. There are RSpec labels
|
||||
available in `spec/support/dns.rb` which you can apply to tests if you need to
|
||||
bypass the DNS stubbing, e.g.:
|
||||
|
||||
```
|
||||
it "really connects to Prometheus", :permit_dns do
|
||||
```
|
||||
|
||||
And if you need more specific control, the DNS blocking is implemented in
|
||||
`spec/support/helpers/dns_helpers.rb` and these methods can be called elsewhere.
|
||||
|
||||
#### Filesystem
|
||||
|
||||
Filesystem data can be roughly split into "repositories", and "everything else".
|
||||
|
|
|
@ -121,7 +121,7 @@ module Banzai
|
|||
|
||||
def object_link_text(object, matches)
|
||||
milestone_link = escape_once(super)
|
||||
reference = object.project&.to_reference(project)
|
||||
reference = object.project&.to_reference_base(project)
|
||||
|
||||
if reference.present?
|
||||
"#{milestone_link} <i>in #{reference}</i>".html_safe
|
||||
|
|
|
@ -104,7 +104,7 @@ module Banzai
|
|||
def link_to_project(project, link_content: nil)
|
||||
url = urls.project_url(project, only_path: context[:only_path])
|
||||
data = data_attribute(project: project.id)
|
||||
content = link_content || project.to_reference_with_postfix
|
||||
content = link_content || project.to_reference
|
||||
|
||||
link_tag(url, data, content, project.name)
|
||||
end
|
||||
|
|
|
@ -35,7 +35,7 @@ module Gitlab
|
|||
:user_count
|
||||
|
||||
def self.declarative_policy_class
|
||||
'ErrorTracking::DetailedErrorPolicy'
|
||||
'ErrorTracking::BasePolicy'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,11 +4,16 @@ module Gitlab
|
|||
module ErrorTracking
|
||||
class Error
|
||||
include ActiveModel::Model
|
||||
include GlobalID::Identification
|
||||
|
||||
attr_accessor :id, :title, :type, :user_count, :count,
|
||||
:first_seen, :last_seen, :message, :culprit,
|
||||
:external_url, :project_id, :project_name, :project_slug,
|
||||
:short_id, :status, :frequency
|
||||
|
||||
def self.declarative_policy_class
|
||||
'ErrorTracking::BasePolicy'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module ErrorTracking
|
||||
class ErrorCollection
|
||||
include GlobalID::Identification
|
||||
|
||||
attr_accessor :issues, :external_url, :project
|
||||
|
||||
alias_attribute :gitlab_project, :project
|
||||
|
||||
def initialize(project:, external_url: nil, issues: [])
|
||||
@project = project
|
||||
@external_url = external_url
|
||||
@issues = issues
|
||||
end
|
||||
|
||||
def self.declarative_policy_class
|
||||
'ErrorTracking::BasePolicy'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
module Gitlab
|
||||
module Graphql
|
||||
module Extensions
|
||||
class ExternallyPaginatedArrayExtension < GraphQL::Schema::Field::ConnectionExtension
|
||||
def resolve(object:, arguments:, context:)
|
||||
yield(object, arguments)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,72 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module ImportExport
|
||||
class ProjectTreeLoader
|
||||
def load(path, dedup_entries: false)
|
||||
tree_hash = ActiveSupport::JSON.decode(IO.read(path))
|
||||
|
||||
if dedup_entries
|
||||
dedup_tree(tree_hash)
|
||||
else
|
||||
tree_hash
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# This function removes duplicate entries from the given tree recursively
|
||||
# by caching nodes it encounters repeatedly. We only consider nodes for
|
||||
# which there can actually be multiple equivalent instances (e.g. strings,
|
||||
# hashes and arrays, but not `nil`s, numbers or booleans.)
|
||||
#
|
||||
# The algorithm uses a recursive depth-first descent with 3 cases, starting
|
||||
# with a root node (the tree/hash itself):
|
||||
# - a node has already been cached; in this case we return it from the cache
|
||||
# - a node has not been cached yet but should be; descend into its children
|
||||
# - a node is neither cached nor qualifies for caching; this is a no-op
|
||||
def dedup_tree(node, nodes_seen = {})
|
||||
if nodes_seen.key?(node) && distinguishable?(node)
|
||||
yield nodes_seen[node]
|
||||
elsif should_dedup?(node)
|
||||
nodes_seen[node] = node
|
||||
|
||||
case node
|
||||
when Array
|
||||
node.each_index do |idx|
|
||||
dedup_tree(node[idx], nodes_seen) do |cached_node|
|
||||
node[idx] = cached_node
|
||||
end
|
||||
end
|
||||
when Hash
|
||||
node.each do |k, v|
|
||||
dedup_tree(v, nodes_seen) do |cached_node|
|
||||
node[k] = cached_node
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
node
|
||||
end
|
||||
end
|
||||
|
||||
# We do not need to consider nodes for which there cannot be multiple instances
|
||||
def should_dedup?(node)
|
||||
node && !(node.is_a?(Numeric) || node.is_a?(TrueClass) || node.is_a?(FalseClass))
|
||||
end
|
||||
|
||||
# We can only safely de-dup values that are distinguishable. True value objects
|
||||
# are always distinguishable by nature. Hashes however can represent entities,
|
||||
# which are identified by ID, not value. We therefore disallow de-duping hashes
|
||||
# that do not have an `id` field, since we might risk dropping entities that
|
||||
# have equal attributes yet different identities.
|
||||
def distinguishable?(node)
|
||||
if node.is_a?(Hash)
|
||||
node.key?('id')
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,15 +3,17 @@
|
|||
module Gitlab
|
||||
module ImportExport
|
||||
class ProjectTreeRestorer
|
||||
LARGE_PROJECT_FILE_SIZE_BYTES = 500.megabyte
|
||||
|
||||
attr_reader :user
|
||||
attr_reader :shared
|
||||
attr_reader :project
|
||||
|
||||
def initialize(user:, shared:, project:)
|
||||
@path = File.join(shared.export_path, 'project.json')
|
||||
@user = user
|
||||
@shared = shared
|
||||
@project = project
|
||||
@tree_loader = ProjectTreeLoader.new
|
||||
end
|
||||
|
||||
def restore
|
||||
|
@ -36,9 +38,16 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def large_project?(path)
|
||||
File.size(path) >= LARGE_PROJECT_FILE_SIZE_BYTES
|
||||
end
|
||||
|
||||
def read_tree_hash
|
||||
json = IO.read(@path)
|
||||
ActiveSupport::JSON.decode(json)
|
||||
path = File.join(@shared.export_path, 'project.json')
|
||||
dedup_entries = large_project?(path) &&
|
||||
Feature.enabled?(:dedup_project_import_metadata, project.group)
|
||||
|
||||
@tree_loader.load(path, dedup_entries: dedup_entries)
|
||||
rescue => e
|
||||
Rails.logger.error("Import/Export error: #{e.message}") # rubocop:disable Gitlab/RailsLogger
|
||||
raise Gitlab::ImportExport::Error.new('Incorrect JSON format')
|
||||
|
|
|
@ -159,7 +159,7 @@ module Gitlab
|
|||
def build_relation(relation_key, relation_definition, data_hash)
|
||||
# TODO: This is hack to not create relation for the author
|
||||
# Rather make `RelationFactory#set_note_author` to take care of that
|
||||
return data_hash if relation_key == 'author'
|
||||
return data_hash if relation_key == 'author' || already_restored?(data_hash)
|
||||
|
||||
# create relation objects recursively for all sub-objects
|
||||
relation_definition.each do |sub_relation_key, sub_relation_definition|
|
||||
|
@ -169,6 +169,13 @@ module Gitlab
|
|||
@relation_factory.create(relation_factory_params(relation_key, data_hash))
|
||||
end
|
||||
|
||||
# Since we update the data hash in place as we restore relation items,
|
||||
# and since we also de-duplicate items, we might encounter items that
|
||||
# have already been restored in a previous iteration.
|
||||
def already_restored?(relation_item)
|
||||
!relation_item.is_a?(Hash)
|
||||
end
|
||||
|
||||
def transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition)
|
||||
sub_data_hash = data_hash[sub_relation_key]
|
||||
return unless sub_data_hash
|
||||
|
|
|
@ -8450,6 +8450,9 @@ msgstr ""
|
|||
msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
|
||||
msgstr ""
|
||||
|
||||
msgid "For renewal instructions %{link_start}view our Licensing FAQ.%{link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Forgot your password?"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -1,41 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :detailed_error_tracking_error, class: 'Gitlab::ErrorTracking::DetailedError' do
|
||||
id { '1' }
|
||||
title { 'title' }
|
||||
type { 'error' }
|
||||
user_count { 1 }
|
||||
count { 2 }
|
||||
first_seen { Time.now.iso8601 }
|
||||
last_seen { Time.now.iso8601 }
|
||||
message { 'message' }
|
||||
culprit { 'culprit' }
|
||||
external_url { 'http://example.com/id' }
|
||||
factory :detailed_error_tracking_error, parent: :error_tracking_error, class: 'Gitlab::ErrorTracking::DetailedError' do
|
||||
gitlab_issue { 'http://gitlab.example.com/issues/1' }
|
||||
external_base_url { 'http://example.com' }
|
||||
project_id { 'project1' }
|
||||
project_name { 'project name' }
|
||||
project_slug { 'project_name' }
|
||||
short_id { 'ID' }
|
||||
status { 'unresolved' }
|
||||
first_release_last_commit { '68c914da9' }
|
||||
last_release_last_commit { '9ad419c86' }
|
||||
first_release_short_version { 'abc123' }
|
||||
last_release_short_version { 'abc123' }
|
||||
first_release_version { '12345678' }
|
||||
tags do
|
||||
{
|
||||
level: 'error',
|
||||
logger: 'rails'
|
||||
}
|
||||
end
|
||||
frequency do
|
||||
[
|
||||
[Time.now.to_i, 10]
|
||||
]
|
||||
end
|
||||
gitlab_issue { 'http://gitlab.example.com/issues/1' }
|
||||
first_release_last_commit { '68c914da9' }
|
||||
last_release_last_commit { '9ad419c86' }
|
||||
first_release_short_version { 'abc123' }
|
||||
last_release_short_version { 'abc123' }
|
||||
first_release_version { '12345678' }
|
||||
|
||||
skip_create
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
FactoryBot.define do
|
||||
factory :error_tracking_error, class: 'Gitlab::ErrorTracking::Error' do
|
||||
id { 'id' }
|
||||
id { '1' }
|
||||
title { 'title' }
|
||||
type { 'error' }
|
||||
user_count { 1 }
|
||||
count { 2 }
|
||||
first_seen { Time.now }
|
||||
last_seen { Time.now }
|
||||
first_seen { Time.now.iso8601 }
|
||||
last_seen { Time.now.iso8601 }
|
||||
message { 'message' }
|
||||
culprit { 'culprit' }
|
||||
external_url { 'http://example.com/id' }
|
||||
|
@ -17,7 +17,11 @@ FactoryBot.define do
|
|||
project_slug { 'project_name' }
|
||||
short_id { 'ID' }
|
||||
status { 'unresolved' }
|
||||
frequency { [] }
|
||||
frequency do
|
||||
[
|
||||
[Time.now.to_i, 10]
|
||||
]
|
||||
end
|
||||
|
||||
skip_create
|
||||
end
|
||||
|
|
|
@ -32,7 +32,7 @@ describe 'issue move to another project' do
|
|||
let(:new_project) { create(:project) }
|
||||
let(:new_project_search) { create(:project) }
|
||||
let(:text) { "Text with #{mr.to_reference}" }
|
||||
let(:cross_reference) { old_project.to_reference(new_project) }
|
||||
let(:cross_reference) { old_project.to_reference_base(new_project) }
|
||||
|
||||
before do
|
||||
old_project.add_reporter(user)
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"simple": 42,
|
||||
"duped_hash_with_id": {
|
||||
"id": 0,
|
||||
"v1": 1
|
||||
},
|
||||
"duped_hash_no_id": {
|
||||
"v1": 1
|
||||
},
|
||||
"duped_array": [
|
||||
"v2"
|
||||
],
|
||||
"array": [
|
||||
{
|
||||
"duped_hash_with_id": {
|
||||
"id": 0,
|
||||
"v1": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"duped_array": [
|
||||
"v2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"duped_hash_no_id": {
|
||||
"v1": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"nested": {
|
||||
"duped_hash_with_id": {
|
||||
"id": 0,
|
||||
"v1": 1
|
||||
},
|
||||
"duped_array": [
|
||||
"v2"
|
||||
],
|
||||
"array": [
|
||||
"don't touch"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import Tracking from '~/tracking';
|
||||
import stubChildren from 'helpers/stub_children';
|
||||
import component from '~/registry/settings/components/settings_form.vue';
|
||||
import { createStore } from '~/registry/settings/store/';
|
||||
|
@ -15,6 +16,9 @@ describe('Settings Form', () => {
|
|||
let dispatchSpy;
|
||||
|
||||
const FORM_ELEMENTS_ID_PREFIX = '#expiration-policy';
|
||||
const trackingPayload = {
|
||||
label: 'docker_container_retention_and_expiration_policies',
|
||||
};
|
||||
|
||||
const GlLoadingIcon = { name: 'gl-loading-icon-stub', template: '<svg></svg>' };
|
||||
|
||||
|
@ -48,6 +52,7 @@ describe('Settings Form', () => {
|
|||
store.dispatch('setInitialState', stringifiedFormOptions);
|
||||
dispatchSpy = jest.spyOn(store, 'dispatch');
|
||||
mountComponent();
|
||||
jest.spyOn(Tracking, 'event');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -118,15 +123,23 @@ describe('Settings Form', () => {
|
|||
beforeEach(() => {
|
||||
form = findForm();
|
||||
});
|
||||
it('cancel has type reset', () => {
|
||||
expect(findCancelButton().attributes('type')).toBe('reset');
|
||||
});
|
||||
|
||||
it('form reset event call the appropriate function', () => {
|
||||
dispatchSpy.mockReturnValue();
|
||||
form.trigger('reset');
|
||||
// expect.any(Object) is necessary because the event payload is passed to the function
|
||||
expect(dispatchSpy).toHaveBeenCalledWith('resetSettings', expect.any(Object));
|
||||
describe('form cancel event', () => {
|
||||
it('has type reset', () => {
|
||||
expect(findCancelButton().attributes('type')).toBe('reset');
|
||||
});
|
||||
|
||||
it('calls the appropriate function', () => {
|
||||
dispatchSpy.mockReturnValue();
|
||||
form.trigger('reset');
|
||||
expect(dispatchSpy).toHaveBeenCalledWith('resetSettings');
|
||||
});
|
||||
|
||||
it('tracks the reset event', () => {
|
||||
dispatchSpy.mockReturnValue();
|
||||
form.trigger('reset');
|
||||
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'reset_form', trackingPayload);
|
||||
});
|
||||
});
|
||||
|
||||
it('save has type submit', () => {
|
||||
|
@ -177,6 +190,12 @@ describe('Settings Form', () => {
|
|||
expect(dispatchSpy).toHaveBeenCalledWith('saveSettings');
|
||||
});
|
||||
|
||||
it('tracks the submit event', () => {
|
||||
dispatchSpy.mockResolvedValue();
|
||||
form.trigger('submit');
|
||||
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'submit_form', trackingPayload);
|
||||
});
|
||||
|
||||
it('show a success toast when submit succeed', () => {
|
||||
dispatchSpy.mockResolvedValue();
|
||||
form.trigger('submit');
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Resolvers::ErrorTracking::SentryErrorCollectionResolver do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
|
||||
let(:list_issues_service) { spy('ErrorTracking::ListIssuesService') }
|
||||
|
||||
before do
|
||||
project.add_developer(current_user)
|
||||
|
||||
allow(ErrorTracking::ListIssuesService)
|
||||
.to receive(:new)
|
||||
.and_return list_issues_service
|
||||
end
|
||||
|
||||
describe '#resolve' do
|
||||
it 'returns an error collection object' do
|
||||
expect(resolve_error_collection).to be_a Gitlab::ErrorTracking::ErrorCollection
|
||||
end
|
||||
|
||||
it 'provides the service url' do
|
||||
fake_url = 'http://test.com'
|
||||
|
||||
expect(list_issues_service)
|
||||
.to receive(:external_url)
|
||||
.and_return(fake_url)
|
||||
|
||||
result = resolve_error_collection
|
||||
expect(result.external_url).to eq fake_url
|
||||
end
|
||||
|
||||
it 'provides the project' do
|
||||
expect(resolve_error_collection.project).to eq project
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resolve_error_collection(context = { current_user: current_user })
|
||||
resolve(described_class, obj: project, args: {}, ctx: context)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,103 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Resolvers::ErrorTracking::SentryErrorsResolver do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:error_collection) { Gitlab::ErrorTracking::ErrorCollection.new(project: project) }
|
||||
|
||||
let(:list_issues_service) { spy('ErrorTracking::ListIssuesService') }
|
||||
|
||||
let(:issues) { nil }
|
||||
let(:pagination) { nil }
|
||||
|
||||
describe '#resolve' do
|
||||
context 'insufficient user permission' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it 'returns nil' do
|
||||
context = { current_user: user }
|
||||
|
||||
expect(resolve_errors({}, context)).to eq nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'user with permission' do
|
||||
before do
|
||||
project.add_developer(current_user)
|
||||
|
||||
allow(ErrorTracking::ListIssuesService)
|
||||
.to receive(:new)
|
||||
.and_return list_issues_service
|
||||
end
|
||||
|
||||
context 'when after arg given' do
|
||||
let(:after) { "1576029072000:0:0" }
|
||||
|
||||
it 'gives the cursor arg' do
|
||||
expect(ErrorTracking::ListIssuesService)
|
||||
.to receive(:new)
|
||||
.with(project, current_user, { cursor: after })
|
||||
.and_return list_issues_service
|
||||
|
||||
resolve_errors({ after: after })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no issues fetched' do
|
||||
before do
|
||||
allow(list_issues_service)
|
||||
.to receive(:execute)
|
||||
.and_return(
|
||||
issues: nil
|
||||
)
|
||||
end
|
||||
it 'returns nil' do
|
||||
expect(resolve_errors).to eq nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issues returned' do
|
||||
let(:issues) { [:issue_1, :issue_2] }
|
||||
let(:pagination) do
|
||||
{
|
||||
'next' => { 'cursor' => 'next' },
|
||||
'previous' => { 'cursor' => 'prev' }
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
allow(list_issues_service)
|
||||
.to receive(:execute)
|
||||
.and_return(
|
||||
issues: issues,
|
||||
pagination: pagination
|
||||
)
|
||||
end
|
||||
|
||||
it 'sets the issues' do
|
||||
expect(resolve_errors).to contain_exactly(*issues)
|
||||
end
|
||||
|
||||
it 'sets the pagination variables' do
|
||||
result = resolve_errors
|
||||
expect(result.next_cursor).to eq 'next'
|
||||
expect(result.previous_cursor).to eq 'prev'
|
||||
end
|
||||
|
||||
it 'returns an externally paginated array' do
|
||||
expect(resolve_errors).to be_a Gitlab::Graphql::ExternallyPaginatedArray
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resolve_errors(args = {}, context = { current_user: current_user })
|
||||
resolve(described_class, obj: error_collection, args: args, ctx: context)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe GitlabSchema.types['SentryErrorCollection'] do
|
||||
it { expect(described_class.graphql_name).to eq('SentryErrorCollection') }
|
||||
|
||||
it { expect(described_class).to require_graphql_authorizations(:read_sentry_issue) }
|
||||
|
||||
it 'exposes the expected fields' do
|
||||
expected_fields = %i[
|
||||
errors
|
||||
detailed_error
|
||||
external_url
|
||||
]
|
||||
|
||||
is_expected.to have_graphql_fields(*expected_fields)
|
||||
end
|
||||
|
||||
describe 'errors field' do
|
||||
subject { described_class.fields['errors'] }
|
||||
|
||||
it 'returns errors' do
|
||||
aggregate_failures 'testing the correct types are returned' do
|
||||
is_expected.to have_graphql_type(Types::ErrorTracking::SentryErrorType.connection_type)
|
||||
is_expected.to have_graphql_extension(Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension)
|
||||
is_expected.to have_graphql_resolver(Resolvers::ErrorTracking::SentryErrorsResolver)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe GitlabSchema.types['SentryError'] do
|
||||
it { expect(described_class.graphql_name).to eq('SentryError') }
|
||||
|
||||
it 'exposes the expected fields' do
|
||||
expected_fields = %i[
|
||||
id
|
||||
sentryId
|
||||
title
|
||||
type
|
||||
userCount
|
||||
count
|
||||
firstSeen
|
||||
lastSeen
|
||||
message
|
||||
culprit
|
||||
externalUrl
|
||||
sentryProjectId
|
||||
sentryProjectName
|
||||
sentryProjectSlug
|
||||
shortId
|
||||
status
|
||||
frequency
|
||||
]
|
||||
|
||||
is_expected.to have_graphql_fields(*expected_fields)
|
||||
end
|
||||
end
|
|
@ -229,10 +229,10 @@ describe Banzai::Filter::CommitRangeReferenceFilter do
|
|||
end
|
||||
|
||||
it 'ignores invalid commit IDs on the referenced project' do
|
||||
exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}"
|
||||
exp = act = "Fixed #{project2.to_reference_base}@#{commit1.id.reverse}...#{commit2.id}"
|
||||
expect(reference_filter(act).to_html).to eq exp
|
||||
|
||||
exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}"
|
||||
exp = act = "Fixed #{project2.to_reference_base}@#{commit1.id}...#{commit2.id.reverse}"
|
||||
expect(reference_filter(act).to_html).to eq exp
|
||||
end
|
||||
end
|
||||
|
|
|
@ -369,7 +369,7 @@ describe Banzai::Filter::LabelReferenceFilter do
|
|||
end
|
||||
|
||||
context 'with project reference' do
|
||||
let(:reference) { "#{project.to_reference}#{group_label.to_reference(format: :name)}" }
|
||||
let(:reference) { "#{project.to_reference_base}#{group_label.to_reference(format: :name)}" }
|
||||
|
||||
it 'links to a valid reference' do
|
||||
doc = reference_filter("See #{reference}", project: project)
|
||||
|
@ -385,7 +385,7 @@ describe Banzai::Filter::LabelReferenceFilter do
|
|||
end
|
||||
|
||||
it 'ignores invalid label names' do
|
||||
exp = act = %(Label #{project.to_reference}#{Label.reference_prefix}"#{group_label.name.reverse}")
|
||||
exp = act = %(Label #{project.to_reference_base}#{Label.reference_prefix}"#{group_label.name.reverse}")
|
||||
|
||||
expect(reference_filter(act).to_html).to eq exp
|
||||
end
|
||||
|
|
|
@ -367,15 +367,17 @@ describe Banzai::Filter::MilestoneReferenceFilter do
|
|||
expect(doc.css('a').first.text).to eq(urls.milestone_url(milestone))
|
||||
end
|
||||
|
||||
it 'does not support cross-project references' do
|
||||
it 'does not support cross-project references', :aggregate_failures do
|
||||
another_group = create(:group)
|
||||
another_project = create(:project, :public, group: group)
|
||||
project_reference = another_project.to_reference(project)
|
||||
project_reference = another_project.to_reference_base(project)
|
||||
input_text = "See #{project_reference}#{reference}"
|
||||
|
||||
milestone.update!(group: another_group)
|
||||
|
||||
doc = reference_filter("See #{project_reference}#{reference}")
|
||||
doc = reference_filter(input_text)
|
||||
|
||||
expect(input_text).to match(Milestone.reference_pattern)
|
||||
expect(doc.css('a')).to be_empty
|
||||
end
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ describe Banzai::Filter::ProjectReferenceFilter do
|
|||
end
|
||||
|
||||
def get_reference(project)
|
||||
project.to_reference_with_postfix
|
||||
project.to_reference
|
||||
end
|
||||
|
||||
let(:project) { create(:project, :public) }
|
||||
|
|
|
@ -8,7 +8,7 @@ describe Gitlab::Gfm::ReferenceRewriter do
|
|||
let(:new_project) { create(:project, name: 'new-project', group: group) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
let(:old_project_ref) { old_project.to_reference(new_project) }
|
||||
let(:old_project_ref) { old_project.to_reference_base(new_project) }
|
||||
let(:text) { 'some text' }
|
||||
|
||||
before do
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::ImportExport::ProjectTreeLoader do
|
||||
let(:fixture) { 'spec/fixtures/lib/gitlab/import_export/with_duplicates.json' }
|
||||
let(:project_tree) { JSON.parse(File.read(fixture)) }
|
||||
|
||||
context 'without de-duplicating entries' do
|
||||
let(:parsed_tree) do
|
||||
subject.load(fixture)
|
||||
end
|
||||
|
||||
it 'parses the JSON into the expected tree' do
|
||||
expect(parsed_tree).to eq(project_tree)
|
||||
end
|
||||
|
||||
it 'does not de-duplicate entries' do
|
||||
expect(parsed_tree['duped_hash_with_id']).not_to be(parsed_tree['array'][0]['duped_hash_with_id'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with de-duplicating entries' do
|
||||
let(:parsed_tree) do
|
||||
subject.load(fixture, dedup_entries: true)
|
||||
end
|
||||
|
||||
it 'parses the JSON into the expected tree' do
|
||||
expect(parsed_tree).to eq(project_tree)
|
||||
end
|
||||
|
||||
it 'de-duplicates equal values' do
|
||||
expect(parsed_tree['duped_hash_with_id']).to be(parsed_tree['array'][0]['duped_hash_with_id'])
|
||||
expect(parsed_tree['duped_hash_with_id']).to be(parsed_tree['nested']['duped_hash_with_id'])
|
||||
expect(parsed_tree['duped_array']).to be(parsed_tree['array'][1]['duped_array'])
|
||||
expect(parsed_tree['duped_array']).to be(parsed_tree['nested']['duped_array'])
|
||||
end
|
||||
|
||||
it 'does not de-duplicate hashes without IDs' do
|
||||
expect(parsed_tree['duped_hash_no_id']).to eq(parsed_tree['array'][2]['duped_hash_no_id'])
|
||||
expect(parsed_tree['duped_hash_no_id']).not_to be(parsed_tree['array'][2]['duped_hash_no_id'])
|
||||
end
|
||||
|
||||
it 'keeps single entries intact' do
|
||||
expect(parsed_tree['simple']).to eq(42)
|
||||
expect(parsed_tree['nested']['array']).to eq(["don't touch"])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -450,7 +450,9 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
|
|||
context 'project.json file access check' do
|
||||
let(:user) { create(:user) }
|
||||
let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
|
||||
let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
|
||||
let(:project_tree_restorer) do
|
||||
described_class.new(user: user, shared: shared, project: project)
|
||||
end
|
||||
let(:restored_project_json) { project_tree_restorer.restore }
|
||||
|
||||
it 'does not read a symlink' do
|
||||
|
@ -725,7 +727,9 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
|
|||
let(:project) { create(:project) }
|
||||
let(:user) { create(:user) }
|
||||
let(:tree_hash) { { 'visibility_level' => visibility } }
|
||||
let(:restorer) { described_class.new(user: user, shared: shared, project: project) }
|
||||
let(:restorer) do
|
||||
described_class.new(user: user, shared: shared, project: project)
|
||||
end
|
||||
|
||||
before do
|
||||
expect(restorer).to receive(:read_tree_hash) { tree_hash }
|
||||
|
|
|
@ -131,23 +131,19 @@ describe Project do
|
|||
end
|
||||
|
||||
context 'when creating a new project' do
|
||||
it 'automatically creates a CI/CD settings row' do
|
||||
project = create(:project)
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
it 'automatically creates a CI/CD settings row' do
|
||||
expect(project.ci_cd_settings).to be_an_instance_of(ProjectCiCdSetting)
|
||||
expect(project.ci_cd_settings).to be_persisted
|
||||
end
|
||||
|
||||
it 'automatically creates a container expiration policy row' do
|
||||
project = create(:project)
|
||||
|
||||
expect(project.container_expiration_policy).to be_an_instance_of(ContainerExpirationPolicy)
|
||||
expect(project.container_expiration_policy).to be_persisted
|
||||
end
|
||||
|
||||
it 'automatically creates a Pages metadata row' do
|
||||
project = create(:project)
|
||||
|
||||
expect(project.pages_metadatum).to be_an_instance_of(ProjectPagesMetadatum)
|
||||
expect(project.pages_metadatum).to be_persisted
|
||||
end
|
||||
|
@ -532,111 +528,114 @@ describe Project do
|
|||
it { is_expected.to delegate_method(:last_pipeline).to(:commit).with_arguments(allow_nil: true) }
|
||||
end
|
||||
|
||||
describe '#to_reference_with_postfix' do
|
||||
it 'returns the full path with reference_postfix' do
|
||||
namespace = create(:namespace, path: 'sample-namespace')
|
||||
project = create(:project, path: 'sample-project', namespace: namespace)
|
||||
describe 'reference methods' do
|
||||
let_it_be(:owner) { create(:user, name: 'Gitlab') }
|
||||
let_it_be(:namespace) { create(:namespace, name: 'Sample namespace', path: 'sample-namespace', owner: owner) }
|
||||
let_it_be(:project) { create(:project, name: 'Sample project', path: 'sample-project', namespace: namespace) }
|
||||
let_it_be(:group) { create(:group, name: 'Group', path: 'sample-group') }
|
||||
let_it_be(:another_project) { create(:project, namespace: namespace) }
|
||||
let_it_be(:another_namespace_project) { create(:project, name: 'another-project') }
|
||||
|
||||
expect(project.to_reference_with_postfix).to eq 'sample-namespace/sample-project>'
|
||||
end
|
||||
end
|
||||
describe '#to_reference' do
|
||||
it 'returns the path with reference_postfix' do
|
||||
expect(project.to_reference).to eq("#{project.full_path}>")
|
||||
end
|
||||
|
||||
describe '#to_reference' do
|
||||
let(:owner) { create(:user, name: 'Gitlab') }
|
||||
let(:namespace) { create(:namespace, path: 'sample-namespace', owner: owner) }
|
||||
let(:project) { create(:project, path: 'sample-project', namespace: namespace) }
|
||||
let(:group) { create(:group, name: 'Group', path: 'sample-group') }
|
||||
it 'returns the path with reference_postfix when arg is self' do
|
||||
expect(project.to_reference(project)).to eq("#{project.full_path}>")
|
||||
end
|
||||
|
||||
context 'when nil argument' do
|
||||
it 'returns nil' do
|
||||
expect(project.to_reference).to be_nil
|
||||
it 'returns the full_path with reference_postfix when full' do
|
||||
expect(project.to_reference(full: true)).to eq("#{project.full_path}>")
|
||||
end
|
||||
|
||||
it 'returns the full_path with reference_postfix when cross-project' do
|
||||
expect(project.to_reference(build_stubbed(:project))).to eq("#{project.full_path}>")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when full is true' do
|
||||
it 'returns complete path to the project' do
|
||||
expect(project.to_reference(full: true)).to eq 'sample-namespace/sample-project'
|
||||
expect(project.to_reference(project, full: true)).to eq 'sample-namespace/sample-project'
|
||||
expect(project.to_reference(group, full: true)).to eq 'sample-namespace/sample-project'
|
||||
describe '#to_reference_base' do
|
||||
context 'when nil argument' do
|
||||
it 'returns nil' do
|
||||
expect(project.to_reference_base).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when same project argument' do
|
||||
it 'returns nil' do
|
||||
expect(project.to_reference(project)).to be_nil
|
||||
context 'when full is true' do
|
||||
it 'returns complete path to the project', :aggregate_failures do
|
||||
be_full_path = eq('sample-namespace/sample-project')
|
||||
|
||||
expect(project.to_reference_base(full: true)).to be_full_path
|
||||
expect(project.to_reference_base(project, full: true)).to be_full_path
|
||||
expect(project.to_reference_base(group, full: true)).to be_full_path
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when cross namespace project argument' do
|
||||
let(:another_namespace_project) { create(:project, name: 'another-project') }
|
||||
|
||||
it 'returns complete path to the project' do
|
||||
expect(project.to_reference(another_namespace_project)).to eq 'sample-namespace/sample-project'
|
||||
context 'when same project argument' do
|
||||
it 'returns nil' do
|
||||
expect(project.to_reference_base(project)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when same namespace / cross-project argument' do
|
||||
let(:another_project) { create(:project, namespace: namespace) }
|
||||
|
||||
it 'returns path to the project' do
|
||||
expect(project.to_reference(another_project)).to eq 'sample-project'
|
||||
context 'when cross namespace project argument' do
|
||||
it 'returns complete path to the project' do
|
||||
expect(project.to_reference_base(another_namespace_project)).to eq 'sample-namespace/sample-project'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when different namespace / cross-project argument' do
|
||||
let(:another_namespace) { create(:namespace, path: 'another-namespace', owner: owner) }
|
||||
let(:another_project) { create(:project, path: 'another-project', namespace: another_namespace) }
|
||||
|
||||
it 'returns full path to the project' do
|
||||
expect(project.to_reference(another_project)).to eq 'sample-namespace/sample-project'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when argument is a namespace' do
|
||||
context 'with same project path' do
|
||||
context 'when same namespace / cross-project argument' do
|
||||
it 'returns path to the project' do
|
||||
expect(project.to_reference(namespace)).to eq 'sample-project'
|
||||
expect(project.to_reference_base(another_project)).to eq 'sample-project'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with different project path' do
|
||||
context 'when different namespace / cross-project argument with same owner' do
|
||||
let(:another_namespace_same_owner) { create(:namespace, path: 'another-namespace', owner: owner) }
|
||||
let(:another_project_same_owner) { create(:project, path: 'another-project', namespace: another_namespace_same_owner) }
|
||||
|
||||
it 'returns full path to the project' do
|
||||
expect(project.to_reference(group)).to eq 'sample-namespace/sample-project'
|
||||
expect(project.to_reference_base(another_project_same_owner)).to eq 'sample-namespace/sample-project'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when argument is a namespace' do
|
||||
context 'with same project path' do
|
||||
it 'returns path to the project' do
|
||||
expect(project.to_reference_base(namespace)).to eq 'sample-project'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with different project path' do
|
||||
it 'returns full path to the project' do
|
||||
expect(project.to_reference_base(group)).to eq 'sample-namespace/sample-project'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#to_human_reference' do
|
||||
let(:owner) { create(:user, name: 'Gitlab') }
|
||||
let(:namespace) { create(:namespace, name: 'Sample namespace', owner: owner) }
|
||||
let(:project) { create(:project, name: 'Sample project', namespace: namespace) }
|
||||
|
||||
context 'when nil argument' do
|
||||
it 'returns nil' do
|
||||
expect(project.to_human_reference).to be_nil
|
||||
describe '#to_human_reference' do
|
||||
context 'when nil argument' do
|
||||
it 'returns nil' do
|
||||
expect(project.to_human_reference).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when same project argument' do
|
||||
it 'returns nil' do
|
||||
expect(project.to_human_reference(project)).to be_nil
|
||||
context 'when same project argument' do
|
||||
it 'returns nil' do
|
||||
expect(project.to_human_reference(project)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when cross namespace project argument' do
|
||||
let(:another_namespace_project) { create(:project, name: 'another-project') }
|
||||
|
||||
it 'returns complete name with namespace of the project' do
|
||||
expect(project.to_human_reference(another_namespace_project)).to eq 'Gitlab / Sample project'
|
||||
context 'when cross namespace project argument' do
|
||||
it 'returns complete name with namespace of the project' do
|
||||
expect(project.to_human_reference(another_namespace_project)).to eq 'Gitlab / Sample project'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when same namespace / cross-project argument' do
|
||||
let(:another_project) { create(:project, namespace: namespace) }
|
||||
|
||||
it 'returns name of the project' do
|
||||
expect(project.to_human_reference(another_project)).to eq 'Sample project'
|
||||
context 'when same namespace / cross-project argument' do
|
||||
it 'returns name of the project' do
|
||||
expect(project.to_human_reference(another_project)).to eq 'Sample project'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
describe SentryDetailedErrorPresenter do
|
||||
describe SentryErrorPresenter do
|
||||
let(:error) { build(:detailed_error_tracking_error) }
|
||||
let(:presenter) { described_class.new(error) }
|
||||
|
||||
|
@ -10,7 +10,7 @@ describe SentryDetailedErrorPresenter do
|
|||
subject { presenter.frequency }
|
||||
|
||||
it 'returns an array of frequency structs' do
|
||||
expect(subject).to include(a_kind_of(SentryDetailedErrorPresenter::FrequencyStruct))
|
||||
expect(subject).to include(a_kind_of(SentryErrorPresenter::FrequencyStruct))
|
||||
end
|
||||
|
||||
it 'converts the times into UTC time objects' do
|
|
@ -0,0 +1,191 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'sentry errors requests' do
|
||||
include GraphqlHelpers
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:project_setting) { create(:project_error_tracking_setting, project: project) }
|
||||
let_it_be(:current_user) { project.owner }
|
||||
|
||||
let(:query) do
|
||||
graphql_query_for(
|
||||
'project',
|
||||
{ 'fullPath' => project.full_path },
|
||||
query_graphql_field('sentryErrors', {}, fields)
|
||||
)
|
||||
end
|
||||
|
||||
describe 'getting a detailed sentry error' do
|
||||
let_it_be(:sentry_detailed_error) { build(:detailed_error_tracking_error) }
|
||||
let(:sentry_gid) { sentry_detailed_error.to_global_id.to_s }
|
||||
|
||||
let(:detailed_fields) do
|
||||
all_graphql_fields_for('SentryDetailedError'.classify)
|
||||
end
|
||||
|
||||
let(:fields) do
|
||||
query_graphql_field('detailedError', { id: sentry_gid }, detailed_fields)
|
||||
end
|
||||
|
||||
let(:error_data) { graphql_data.dig('project', 'sentryErrors', 'detailedError') }
|
||||
|
||||
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(error_data).to eq nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'reactive cache returns data' do
|
||||
before do
|
||||
allow_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting)
|
||||
.to receive(:issue_details)
|
||||
.and_return({ issue: sentry_detailed_error })
|
||||
|
||||
post_graphql(query, current_user: current_user)
|
||||
end
|
||||
|
||||
let(:sentry_error) { sentry_detailed_error }
|
||||
let(:error) { error_data }
|
||||
|
||||
it_behaves_like 'setting sentry error data'
|
||||
|
||||
it 'is expected to return the frequency correctly' do
|
||||
aggregate_failures 'it returns the frequency correctly' do
|
||||
expect(error_data['frequency'].count).to eql sentry_detailed_error.frequency.count
|
||||
|
||||
first_frequency = error_data['frequency'].first
|
||||
expect(Time.parse(first_frequency['time'])).to eql Time.at(sentry_detailed_error.frequency[0][0], in: 0)
|
||||
expect(first_frequency['count']).to eql sentry_detailed_error.frequency[0][1]
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'sentry api returns an error' do
|
||||
before do
|
||||
expect_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting)
|
||||
.to receive(:issue_details)
|
||||
.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
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'getting an errors list' do
|
||||
let_it_be(:sentry_error) { build(:error_tracking_error) }
|
||||
let_it_be(:pagination) do
|
||||
{
|
||||
'next' => { 'cursor' => '2222' },
|
||||
'previous' => { 'cursor' => '1111' }
|
||||
}
|
||||
end
|
||||
|
||||
let(:fields) do
|
||||
<<~QUERY
|
||||
errors {
|
||||
nodes {
|
||||
#{all_graphql_fields_for('SentryError'.classify)}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
QUERY
|
||||
end
|
||||
|
||||
let(:error_data) { graphql_data.dig('project', 'sentryErrors', 'errors', 'nodes') }
|
||||
let(:pagination_data) { graphql_data.dig('project', 'sentryErrors', 'errors', 'pageInfo') }
|
||||
|
||||
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 nil" do
|
||||
expect(error_data).to eq nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'reactive cache returns data' do
|
||||
before do
|
||||
expect_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting)
|
||||
.to receive(:list_sentry_issues)
|
||||
.and_return({ issues: [sentry_error], pagination: pagination })
|
||||
|
||||
post_graphql(query, current_user: current_user)
|
||||
end
|
||||
|
||||
let(:error) { error_data.first }
|
||||
|
||||
it 'is expected to return an array of data' do
|
||||
expect(error_data).to be_a Array
|
||||
expect(error_data.count).to eq 1
|
||||
end
|
||||
|
||||
it_behaves_like 'setting sentry error data'
|
||||
|
||||
it 'sets the pagination correctly' do
|
||||
expect(pagination_data['startCursor']).to eq(pagination['previous']['cursor'])
|
||||
expect(pagination_data['endCursor']).to eq(pagination['next']['cursor'])
|
||||
end
|
||||
|
||||
it 'is expected to return the frequency correctly' do
|
||||
aggregate_failures 'it returns the frequency correctly' do
|
||||
error = error_data.first
|
||||
|
||||
expect(error['frequency'].count).to eql sentry_error.frequency.count
|
||||
|
||||
first_frequency = error['frequency'].first
|
||||
|
||||
expect(Time.parse(first_frequency['time'])).to eql Time.at(sentry_error.frequency[0][0], in: 0)
|
||||
expect(first_frequency['count']).to eql sentry_error.frequency[0][1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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' })
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -68,6 +68,8 @@ describe 'Self-Monitoring project requests' do
|
|||
let(:job_id) { nil }
|
||||
|
||||
it 'returns bad_request' do
|
||||
create(:application_setting)
|
||||
|
||||
subject
|
||||
|
||||
aggregate_failures do
|
||||
|
@ -81,11 +83,10 @@ describe 'Self-Monitoring project requests' do
|
|||
end
|
||||
|
||||
context 'when self-monitoring project exists' do
|
||||
let(:project) { build(:project) }
|
||||
let(:project) { create(:project) }
|
||||
|
||||
before do
|
||||
stub_application_setting(self_monitoring_project_id: 1)
|
||||
stub_application_setting(self_monitoring_project: project)
|
||||
create(:application_setting, self_monitoring_project_id: project.id)
|
||||
end
|
||||
|
||||
it 'does not need job_id' do
|
||||
|
@ -94,7 +95,7 @@ describe 'Self-Monitoring project requests' do
|
|||
aggregate_failures do
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(json_response).to eq(
|
||||
'project_id' => 1,
|
||||
'project_id' => project.id,
|
||||
'project_full_path' => project.full_path
|
||||
)
|
||||
end
|
||||
|
@ -106,7 +107,7 @@ describe 'Self-Monitoring project requests' do
|
|||
aggregate_failures do
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(json_response).to eq(
|
||||
'project_id' => 1,
|
||||
'project_id' => project.id,
|
||||
'project_full_path' => project.full_path
|
||||
)
|
||||
end
|
||||
|
@ -179,7 +180,7 @@ describe 'Self-Monitoring project requests' do
|
|||
|
||||
context 'when self-monitoring project exists and job does not exist' do
|
||||
before do
|
||||
stub_application_setting(self_monitoring_project_id: 1)
|
||||
create(:application_setting, self_monitoring_project_id: create(:project).id)
|
||||
end
|
||||
|
||||
it 'returns bad_request' do
|
||||
|
@ -196,6 +197,10 @@ describe 'Self-Monitoring project requests' do
|
|||
end
|
||||
|
||||
context 'when self-monitoring project does not exist' do
|
||||
before do
|
||||
create(:application_setting)
|
||||
end
|
||||
|
||||
it 'does not need job_id' do
|
||||
get status_delete_self_monitoring_project_admin_application_settings_path
|
||||
|
||||
|
|
|
@ -108,6 +108,12 @@ RSpec::Matchers.define :have_graphql_resolver do |expected|
|
|||
end
|
||||
end
|
||||
|
||||
RSpec::Matchers.define :have_graphql_extension do |expected|
|
||||
match do |field|
|
||||
expect(field.metadata[:type_class].extensions).to include(expected)
|
||||
end
|
||||
end
|
||||
|
||||
RSpec::Matchers.define :expose_permissions_using do |expected|
|
||||
match do |type|
|
||||
permission_field = type.fields['userPermissions']
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue