Commit ae72d71d authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 759bab05
...@@ -20,11 +20,11 @@ class ApplicationController < ActionController::Base ...@@ -20,11 +20,11 @@ class ApplicationController < ActionController::Base
before_action :authenticate_user!, except: [:route_not_found] before_action :authenticate_user!, except: [:route_not_found]
before_action :enforce_terms!, if: :should_enforce_terms? before_action :enforce_terms!, if: :should_enforce_terms?
before_action :validate_user_service_ticket! before_action :validate_user_service_ticket!
before_action :check_password_expiration before_action :check_password_expiration, if: :html_request?
before_action :ldap_security_check before_action :ldap_security_check
before_action :sentry_context before_action :sentry_context
before_action :default_headers before_action :default_headers
before_action :add_gon_variables, unless: [:peek_request?, :json_request?] before_action :add_gon_variables, if: :html_request?
before_action :configure_permitted_parameters, if: :devise_controller? before_action :configure_permitted_parameters, if: :devise_controller?
before_action :require_email, unless: :devise_controller? before_action :require_email, unless: :devise_controller?
before_action :active_user_check, unless: :devise_controller? before_action :active_user_check, unless: :devise_controller?
...@@ -455,8 +455,8 @@ class ApplicationController < ActionController::Base ...@@ -455,8 +455,8 @@ class ApplicationController < ActionController::Base
response.headers['Page-Title'] = URI.escape(page_title('GitLab')) response.headers['Page-Title'] = URI.escape(page_title('GitLab'))
end end
def peek_request? def html_request?
request.path.start_with?('/-/peek') request.format.html?
end end
def json_request? def json_request?
...@@ -466,7 +466,7 @@ class ApplicationController < ActionController::Base ...@@ -466,7 +466,7 @@ class ApplicationController < ActionController::Base
def should_enforce_terms? def should_enforce_terms?
return false unless Gitlab::CurrentSettings.current_application_settings.enforce_terms return false unless Gitlab::CurrentSettings.current_application_settings.enforce_terms
!(peek_request? || devise_controller?) html_request? && !devise_controller?
end end
def set_usage_stats_consent_flag def set_usage_stats_consent_flag
......
...@@ -4,15 +4,18 @@ module ConfirmEmailWarning ...@@ -4,15 +4,18 @@ module ConfirmEmailWarning
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
before_action :set_confirm_warning, if: -> { Feature.enabled?(:soft_email_confirmation) } before_action :set_confirm_warning, if: :show_confirm_warning?
end end
protected protected
def show_confirm_warning?
html_request? && request.get? && Feature.enabled?(:soft_email_confirmation)
end
def set_confirm_warning def set_confirm_warning
return unless current_user return unless current_user
return if current_user.confirmed? return if current_user.confirmed?
return if peek_request? || json_request? || !request.get?
email = current_user.unconfirmed_email || current_user.email email = current_user.unconfirmed_email || current_user.email
......
# frozen_string_literal: true # frozen_string_literal: true
module UploadsActions module UploadsActions
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include SendFileUpload include SendFileUpload
UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo favicon).freeze UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo favicon).freeze
included do
prepend_before_action :set_request_format_from_path_extension
end
def create def create
uploader = UploadService.new(model, params[:file], uploader_class).execute uploader = UploadService.new(model, params[:file], uploader_class).execute
...@@ -64,6 +69,18 @@ module UploadsActions ...@@ -64,6 +69,18 @@ module UploadsActions
private private
# From ActionDispatch::Http::MimeNegotiation. We have an initializer that
# monkey-patches this method out (so that repository paths don't guess a
# format based on extension), but we do want this behaviour when serving
# uploads.
def set_request_format_from_path_extension
path = request.headers['action_dispatch.original_path'] || request.headers['PATH_INFO']
if match = path&.match(/\.(\w+)\z/)
request.format = match.captures.first
end
end
def uploader_class def uploader_class
raise NotImplementedError raise NotImplementedError
end end
......
...@@ -133,7 +133,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -133,7 +133,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
if environment if environment
redirect_to environment_metrics_path(environment) redirect_to environment_metrics_path(environment)
else else
render :empty render :empty_metrics
end end
end end
......
...@@ -15,6 +15,23 @@ class Projects::ErrorTrackingController < Projects::ApplicationController ...@@ -15,6 +15,23 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
end end
end end
def details
respond_to do |format|
format.html
format.json do
render_issue_detail_json
end
end
end
def stack_trace
respond_to do |format|
format.json do
render_issue_stack_trace_json
end
end
end
def list_projects def list_projects
respond_to do |format| respond_to do |format|
format.json do format.json do
...@@ -29,10 +46,7 @@ class Projects::ErrorTrackingController < Projects::ApplicationController ...@@ -29,10 +46,7 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
service = ErrorTracking::ListIssuesService.new(project, current_user) service = ErrorTracking::ListIssuesService.new(project, current_user)
result = service.execute result = service.execute
unless result[:status] == :success return if handle_errors(result)
return render json: { message: result[:message] },
status: result[:http_status] || :bad_request
end
render json: { render json: {
errors: serialize_errors(result[:issues]), errors: serialize_errors(result[:issues]),
...@@ -40,6 +54,28 @@ class Projects::ErrorTrackingController < Projects::ApplicationController ...@@ -40,6 +54,28 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
} }
end end
def render_issue_detail_json
service = ErrorTracking::IssueDetailsService.new(project, current_user, issue_details_params)
result = service.execute
return if handle_errors(result)
render json: {
error: serialize_detailed_error(result[:issue])
}
end
def render_issue_stack_trace_json
service = ErrorTracking::IssueLatestEventService.new(project, current_user, issue_details_params)
result = service.execute
return if handle_errors(result)
render json: {
error: serialize_error_event(result[:latest_event])
}
end
def render_project_list_json def render_project_list_json
service = ErrorTracking::ListProjectsService.new( service = ErrorTracking::ListProjectsService.new(
project, project,
...@@ -62,10 +98,21 @@ class Projects::ErrorTrackingController < Projects::ApplicationController ...@@ -62,10 +98,21 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
end end
end end
def handle_errors(result)
unless result[:status] == :success
render json: { message: result[:message] },
status: result[:http_status] || :bad_request
end
end
def list_projects_params def list_projects_params
params.require(:error_tracking_setting).permit([:api_host, :token]) params.require(:error_tracking_setting).permit([:api_host, :token])
end end
def issue_details_params
params.permit(:issue_id)
end
def set_polling_interval def set_polling_interval
Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL) Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL)
end end
...@@ -76,6 +123,18 @@ class Projects::ErrorTrackingController < Projects::ApplicationController ...@@ -76,6 +123,18 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
.represent(errors) .represent(errors)
end end
def serialize_detailed_error(error)
ErrorTracking::DetailedErrorSerializer
.new(project: project, user: current_user)
.represent(error)
end
def serialize_error_event(event)
ErrorTracking::ErrorEventSerializer
.new(project: project, user: current_user)
.represent(event)
end
def serialize_projects(projects) def serialize_projects(projects)
ErrorTracking::ProjectSerializer ErrorTracking::ProjectSerializer
.new(project: project, user: current_user) .new(project: project, user: current_user)
......
...@@ -20,7 +20,7 @@ class UploadsController < ApplicationController ...@@ -20,7 +20,7 @@ class UploadsController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
before_action :upload_mount_satisfied? before_action :upload_mount_satisfied?
before_action :find_model before_action :model
before_action :authorize_access!, only: [:show] before_action :authorize_access!, only: [:show]
before_action :authorize_create_access!, only: [:create, :authorize] before_action :authorize_create_access!, only: [:create, :authorize]
before_action :verify_workhorse_api!, only: [:authorize] before_action :verify_workhorse_api!, only: [:authorize]
......
...@@ -293,7 +293,7 @@ module ApplicationSettingsHelper ...@@ -293,7 +293,7 @@ module ApplicationSettingsHelper
:snowplow_collector_hostname, :snowplow_collector_hostname,
:snowplow_cookie_domain, :snowplow_cookie_domain,
:snowplow_enabled, :snowplow_enabled,
:snowplow_site_id, :snowplow_app_id,
:snowplow_iglu_registry_url, :snowplow_iglu_registry_url,
:push_event_hooks_limit, :push_event_hooks_limit,
:push_event_activities_limit, :push_event_activities_limit,
......
...@@ -13,4 +13,13 @@ module Projects::ErrorTrackingHelper ...@@ -13,4 +13,13 @@ module Projects::ErrorTrackingHelper
'illustration-path' => image_path('illustrations/cluster_popover.svg') 'illustration-path' => image_path('illustrations/cluster_popover.svg')
} }
end end
def error_details_data(project, issue)
opts = [project, issue, { format: :json }]
{
'issue-details-path' => details_namespace_project_error_tracking_index_path(*opts),
'issue-stack-trace-path' => stack_trace_namespace_project_error_tracking_index_path(*opts)
}
end
end end
...@@ -132,11 +132,12 @@ module ApplicationSettingImplementation ...@@ -132,11 +132,12 @@ module ApplicationSettingImplementation
snowplow_collector_hostname: nil, snowplow_collector_hostname: nil,
snowplow_cookie_domain: nil, snowplow_cookie_domain: nil,
snowplow_enabled: false, snowplow_enabled: false,
snowplow_site_id: nil, snowplow_app_id: nil,
snowplow_iglu_registry_url: nil, snowplow_iglu_registry_url: nil,
custom_http_clone_url_root: nil, custom_http_clone_url_root: nil,
pendo_enabled: false, pendo_enabled: false,
pendo_url: nil pendo_url: nil,
productivity_analytics_start_date: Time.now
} }
end end
......
...@@ -87,10 +87,30 @@ module ErrorTracking ...@@ -87,10 +87,30 @@ module ErrorTracking
{ projects: sentry_client.list_projects } { projects: sentry_client.list_projects }
end end
def issue_details(opts = {})
with_reactive_cache('issue_details', opts.stringify_keys) do |result|
result
end
end
def issue_latest_event(opts = {})
with_reactive_cache('issue_latest_event', opts.stringify_keys) do |result|
result
end
end
def calculate_reactive_cache(request, opts) def calculate_reactive_cache(request, opts)
case request case request
when 'list_issues' when 'list_issues'
{ issues: sentry_client.list_issues(**opts.symbolize_keys) } { issues: sentry_client.list_issues(**opts.symbolize_keys) }
when 'issue_details'
{
issue: sentry_client.issue_details(**opts.symbolize_keys)
}
when 'issue_latest_event'
{
latest_event: sentry_client.issue_latest_event(**opts.symbolize_keys)
}
end end
rescue Sentry::Client::Error => e rescue Sentry::Client::Error => e
{ error: e.message, error_type: SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE } { error: e.message, error_type: SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE }
......
# frozen_string_literal: true
module ErrorTracking
class DetailedErrorEntity < Grape::Entity
expose :count,
:culprit,
:external_base_url,
:external_url,
:first_release_last_commit,
:first_release_short_version,
:first_seen,
:frequency,
:id,
:last_release_last_commit,
:last_release_short_version,
:last_seen,
:message,
:project_id,
:project_name,
:project_slug,
:short_id,
:status,
:title,
:type,
:user_count
end
end
# frozen_string_literal: true
module ErrorTracking
class DetailedErrorSerializer < BaseSerializer
entity DetailedErrorEntity
end
end
# frozen_string_literal: true
module ErrorTracking
class ErrorEventEntity < Grape::Entity
expose :issue_id, :date_received, :stack_trace_entries
end
end
# frozen_string_literal: true
module ErrorTracking
class ErrorEventSerializer < BaseSerializer
entity ErrorEventEntity
end
end
# frozen_string_literal: true
module ErrorTracking
class BaseService < ::BaseService
def execute
unauthorized = check_permissions
return unauthorized if unauthorized
begin
response = fetch
rescue Sentry::Client::Error => e
return error(e.message, :bad_request)
rescue Sentry::Client::MissingKeysError => e
return error(e.message, :internal_server_error)
end
errors = parse_errors(response)
return errors if errors
success(parse_response(response))
end
private
def fetch
raise NotImplementedError,
"#{self.class} does not implement #{__method__}"
end
def parse_response(response)
raise NotImplementedError,
"#{self.class} does not implement #{__method__}"
end
def check_permissions
return error('Error Tracking is not enabled') unless enabled?
return error('Access denied', :unauthorized) unless can_read?
end
def parse_errors(response)
return error('Not ready. Try again later', :no_content) unless response
return error(response[:error], http_status_for(response[:error_type])) if response[:error].present?
end
def http_status_for(error_type)
case error_type
when ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_MISSING_KEYS
:internal_server_error
else
:bad_request
end
end
def project_error_tracking_setting
project.error_tracking_setting
end
def enabled?
project_error_tracking_setting&.enabled?
end
def can_read?
can?(current_user, :read_sentry_issue, project)
end
end
end
# frozen_string_literal: true
module ErrorTracking
class IssueDetailsService < ErrorTracking::BaseService
private
def fetch
project_error_tracking_setting.issue_details(issue_id: params[:issue_id])
end
def parse_response(response)
{ issue: response[:issue] }
end
end
end
# frozen_string_literal: true
module ErrorTracking
class IssueLatestEventService < ErrorTracking::BaseService
private
def fetch
project_error_tracking_setting.issue_latest_event(issue_id: params[:issue_id])
end
def parse_response(response)
{ latest_event: response[:latest_event] }
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
module ErrorTracking module ErrorTracking
class ListIssuesService < ::BaseService class ListIssuesService < ErrorTracking::BaseService
DEFAULT_ISSUE_STATUS = 'unresolved' DEFAULT_ISSUE_STATUS = 'unresolved'
DEFAULT_LIMIT = 20 DEFAULT_LIMIT = 20
def execute private
return error('Error Tracking is not enabled') unless enabled?
return error('Access denied', :unauthorized) unless can_read?
result = project_error_tracking_setting
.list_sentry_issues(issue_status: issue_status, limit: limit)
# our results are not yet ready
unless result
return error('Not ready. Try again later', :no_content)
end
if result[:error].present? def fetch
return error(result[:error], http_status_from_error_type(result[:error_type])) project_error_tracking_setting.list_sentry_issues(issue_status: issue_status, limit: limit)
end end
success(issues: result[:issues]) def parse_response(response)
{ issues: response[:issues] }
end end
def external_url def external_url
project_error_tracking_setting&.sentry_external_url project_error_tracking_setting&.sentry_external_url
end end
private
def http_status_from_error_type(error_type)
case error_type
when ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_MISSING_KEYS
:internal_server_error
else
:bad_request
end
end
def project_error_tracking_setting
project.error_tracking_setting
end
def issue_status def issue_status
params[:issue_status] || DEFAULT_ISSUE_STATUS params[:issue_status] || DEFAULT_ISSUE_STATUS
end end
...@@ -50,13 +26,5 @@ module ErrorTracking ...@@ -50,13 +26,5 @@ module ErrorTracking
def limit def limit
params[:limit] || DEFAULT_LIMIT params[:limit] || DEFAULT_LIMIT
end end
def enabled?
project_error_tracking_setting&.enabled?
end
def can_read?
can?(current_user, :read_sentry_issue, project)
end
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
module ErrorTracking module ErrorTracking
class ListProjectsService < ::BaseService class ListProjectsService < ErrorTracking::BaseService
def execute def execute
return error('access denied') unless can_read? unless project_error_tracking_setting.valid?
return error(project_error_tracking_setting.errors.full_messages.join(', '), :bad_request)
setting = project_error_tracking_setting
unless setting.valid?
return error(setting.errors.full_messages.join(', '), :bad_request)
end end
begin super
result = setting.list_sentry_projects
rescue Sentry::Client::Error => e
return error(e.message, :bad_request)
rescue Sentry::Client::MissingKeysError => e
return error(e.message, :internal_server_error)
end
success(projects: result[:projects])
end end
private private
def project_error_tracking_setting def fetch
(project.error_tracking_setting || project.build_error_tracking_setting).tap do |setting| project_error_tracking_setting.list_sentry_projects
setting.api_url = ErrorTracking::ProjectErrorTrackingSetting.build_api_url_from( end
api_host: params[:api_host],
organization_slug: 'org', def parse_response(response)
project_slug: 'proj' { projects: response[:projects] }
)
setting.token = token(setting)
setting.enabled = true
end
end end
def can_read? def project_error_tracking_setting
can?(current_user, :read_sentry_issue, project) @project_error_tracking_setting ||= begin
(super || project.build_error_tracking_setting).tap do |setting|
setting.api_url = ErrorTracking::ProjectErrorTrackingSetting.build_api_url_from(
api_host: params[:api_host],
organization_slug: 'org',
project_slug: 'proj'
)
setting.token = token(setting)
setting.enabled = true
end
end
end end
def token(setting) def token(setting)
......
...@@ -21,8 +21,8 @@ ...@@ -21,8 +21,8 @@
= f.label :snowplow_collector_hostname, _('Collector hostname'), class: 'label-light' = f.label :snowplow_collector_hostname, _('Collector hostname'), class: 'label-light'
= f.text_field :snowplow_collector_hostname, class: 'form-control', placeholder: 'snowplow.example.com' = f.text_field :snowplow_collector_hostname, class: 'form-control', placeholder: 'snowplow.example.com'
.form-group .form-group
= f.label :snowplow_site_id, _('Site ID'), class: 'label-light' = f.label :snowplow_app_id, _('App ID'), class: 'label-light'
= f.text_field :snowplow_site_id, class: 'form-control' = f.text_field :snowplow_app_id, class: 'form-control'
.form-group .form-group
= f.label :snowplow_cookie_domain, _('Cookie domain'), class: 'label-light' = f.label :snowplow_cookie_domain, _('Cookie domain'), class: 'label-light'
= f.text_field :snowplow_cookie_domain, class: 'form-control' = f.text_field :snowplow_cookie_domain, class: 'form-control'
......
...@@ -247,6 +247,8 @@ ...@@ -247,6 +247,8 @@
%span %span
= _('Serverless') = _('Serverless')
= render_if_exists 'layouts/nav/sidebar/pod_logs_link' # EE-specific
- if project_nav_tab? :clusters - if project_nav_tab? :clusters
- show_cluster_hint = show_gke_cluster_integration_callout?(@project) - show_cluster_hint = show_gke_cluster_integration_callout?(@project)
= nav_link(controller: [:clusters, :user, :gcp]) do = nav_link(controller: [:clusters, :user, :gcp]) do
......
- page_title _('Pod logs')
.row.empty-state
.col-sm-12
.svg-content
= image_tag 'illustrations/operations_log_pods_empty.svg'
.col-12
.text-content
%h4.text-center
= s_('Environments|No deployed environments')
%p.state-description.text-center
= s_('Logs|To see the pod logs, deploy your code to an environment.')
.text-center
= link_to s_('Environments|Learn about environments'), help_page_path('ci/environments'), class: 'btn btn-success'
...@@ -7,8 +7,8 @@ ...@@ -7,8 +7,8 @@
.col-12 .col-12
.text-content .text-content
%h4.text-center %h4.text-center
= s_('Metrics|No deployed environments') = s_('Environments|No deployed environments')
%p.state-description %p.state-description
= s_('Metrics|Check out the CI/CD documentation on deploying to an environment') = s_('Metrics|Check out the CI/CD documentation on deploying to an environment')
.text-center .text-center
= link_to s_("Metrics|Learn about environments"), help_page_path('ci/environments'), class: 'btn btn-success' = link_to s_("Environments|Learn about environments"), help_page_path('ci/environments'), class: 'btn btn-success'
- page_title _('Error Details')
#js-error_tracking{ data: error_details_data(@current_user, @project) }
---
title: Rename snowplow_site_id to snowplow_app_id in application_settings table
merge_request: 19252
author:
type: other
---
title: Add productivity analytics merge date filtering limit
merge_request: 32052
author:
type: fixed
---
title: API for stack trace & detail view of Sentry error in GitLab
merge_request: 19137
author:
type: added
...@@ -441,6 +441,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -441,6 +441,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :metrics, action: :metrics_redirect get :metrics, action: :metrics_redirect
get :folder, path: 'folders/*id', constraints: { format: /(html|json)/ } get :folder, path: 'folders/*id', constraints: { format: /(html|json)/ }
get :search get :search
Gitlab.ee do
get :logs, action: :logs_redirect
end
end end
resources :deployments, only: [:index] do resources :deployments, only: [:index] do
...@@ -613,6 +617,12 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -613,6 +617,12 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :error_tracking, only: [:index], controller: :error_tracking do resources :error_tracking, only: [:index], controller: :error_tracking do
collection do collection do
get ':issue_id/details',
to: 'error_tracking#details',
as: 'details'
get ':issue_id/stack_trace',
to: 'error_tracking#stack_trace',
as: 'stack_trace'
post :list_projects post :list_projects
end end
end end
......
# frozen_string_literal: true
class AddProductivityAnalyticsStartDate < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :application_settings, :productivity_analytics_start_date, :datetime_with_timezone
end
end
# frozen_string_literal: true
# Expected migration duration: 1 minute
class FillProductivityAnalyticsStartDate < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :merge_request_metrics, :merged_at,
where: "merged_at > '2019-09-01' AND commits_count IS NOT NULL",
name: 'fill_productivity_analytics_start_date_tmp_index'
execute(
<<SQL
UPDATE application_settings
SET productivity_analytics_start_date = COALESCE((SELECT MIN(merged_at) FROM merge_request_metrics
WHERE merged_at > '2019-09-01' AND commits_count IS NOT NULL), NOW())
SQL
)
remove_concurrent_index :merge_request_metrics, :merged_at,
name: 'fill_productivity_analytics_start_date_tmp_index'
end
def down
execute('UPDATE application_settings SET productivity_analytics_start_date = NULL')
end
end
# frozen_string_literal: true
class RenameSnowplowSiteIdToSnowplowAppId < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
rename_column_concurrently :application_settings, :snowplow_site_id, :snowplow_app_id
end
def down
undo_rename_column_concurrently :application_settings, :snowplow_site_id, :snowplow_app_id
end
end
# frozen_string_literal: true
class CleanupApplicationSettingsSnowplowSiteIdRename < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
cleanup_concurrent_column_rename :application_settings, :snowplow_site_id, :snowplow_app_id
end
def down
undo_cleanup_concurrent_column_rename :application_settings, :snowplow_site_id, :snowplow_app_id
end
end
...@@ -287,7 +287,6 @@ ActiveRecord::Schema.define(version: 2019_11_05_094625) do ...@@ -287,7 +287,6 @@ ActiveRecord::Schema.define(version: 2019_11_05_094625) do
t.boolean "hide_third_party_offers", default: false, null: false t.boolean "hide_third_party_offers", default: false, null: false
t.boolean "snowplow_enabled", default: false, null: false t.boolean "snowplow_enabled", default: false, null: false
t.string "snowplow_collector_hostname" t.string "snowplow_collector_hostname"
t.string "snowplow_site_id"
t.string "snowplow_cookie_domain" t.string "snowplow_cookie_domain"
t.boolean "instance_statistics_visibility_private", default: false, null: false t.boolean "instance_statistics_visibility_private", default: false, null: false
t.boolean "web_ide_clientside_preview_enabled", default: false, null: false t.boolean "web_ide_clientside_preview_enabled", default: false, null: false
...@@ -350,6 +349,8 @@ ActiveRecord::Schema.define(version: 2019_11_05_094625) do ...@@ -350,6 +349,8 @@ ActiveRecord::Schema.define(version: 2019_11_05_094625) do
t.string "eks_access_key_id", limit: 128 t.string "eks_access_key_id", limit: 128
t.string "encrypted_eks_secret_access_key_iv", limit: 255 t.string "encrypted_eks_secret_access_key_iv", limit: 255
t.text "encrypted_eks_secret_access_key" t.text "encrypted_eks_secret_access_key"
t.string "snowplow_app_id"
t.datetime_with_timezone "productivity_analytics_start_date"
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id" t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id"
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id" t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id"
t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id" t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id"
......
...@@ -319,7 +319,7 @@ are listed in the descriptions of the relevant settings. ...@@ -319,7 +319,7 @@ are listed in the descriptions of the relevant settings.
| `snowplow_collector_hostname` | string | required by: `snowplow_enabled` | The Snowplow collector hostname. (e.g. `snowplow.trx.gitlab.net`) | | `snowplow_collector_hostname` | string | required by: `snowplow_enabled` | The Snowplow collector hostname. (e.g. `snowplow.trx.gitlab.net`) |
| `snowplow_cookie_domain` | string | no | The Snowplow cookie domain. (e.g. `.gitlab.com`) | | `snowplow_cookie_domain` | string | no | The Snowplow cookie domain. (e.g. `.gitlab.com`) |
| `snowplow_enabled` | boolean | no | Enable snowplow tracking. | | `snowplow_enabled` | boolean | no | Enable snowplow tracking. |
| `snowplow_site_id` | string | no | The Snowplow site name / application id. (e.g. `gitlab`) | | `snowplow_app_id` | string | no | The Snowplow site name / application id. (e.g. `gitlab`) |
| `snowplow_iglu_registry_url` | string | no | The Snowplow base Iglu Schema Registry URL to use for custom context and self describing events'| | `snowplow_iglu_registry_url` | string | no | The Snowplow base Iglu Schema Registry URL to use for custom context and self describing events'|
| `pendo_url` | string | required by: `pendo_enabled` | The Pendo endpoint url with js snippet. (e.g. `https://cdn.pendo.io/agent/static/your-api-key/pendo.js`) | | `pendo_url` | string | required by: `pendo_enabled` | The Pendo endpoint url with js snippet. (e.g. `https://cdn.pendo.io/agent/static/your-api-key/pendo.js`) |
| `pendo_enabled` | boolean | no | Enable pendo tracking. | | `pendo_enabled` | boolean | no | Enable pendo tracking. |
......
...@@ -135,6 +135,7 @@ The following job parameters can be defined inside a `default:` block: ...@@ -135,6 +135,7 @@ The following job parameters can be defined inside a `default:` block:
- [`before_script`](#before_script-and-after_script) - [`before_script`](#before_script-and-after_script)
- [`after_script`](#before_script-and-after_script) - [`after_script`](#before_script-and-after_script)
- [`cache`](#cache) - [`cache`](#cache)
- [`interruptible`](#interruptible)
In the following example, the `ruby:2.5` image is set as the default for all In the following example, the `ruby:2.5` image is set as the default for all
jobs except the `rspec 2.6` job, which uses the `ruby:2.6` image: jobs except the `rspec 2.6` job, which uses the `ruby:2.6` image:
......
...@@ -296,9 +296,12 @@ module API ...@@ -296,9 +296,12 @@ module API
end end
get ':id/merge_requests/:merge_request_iid/commits' do get ':id/merge_requests/:merge_request_iid/commits' do
merge_request = find_merge_request_with_access(params[:merge_request_iid]) merge_request = find_merge_request_with_access(params[:merge_request_iid])
commits = ::Kaminari.paginate_array(merge_request.commits)
present paginate(commits), with: Entities::Commit commits =
paginate(merge_request.merge_request_diff.merge_request_diff_commits)
.map { |commit| Commit.from_hash(commit.to_hash, merge_request.project) }
present commits, with: Entities::Commit
end end
desc 'Show the merge request changes' do desc 'Show the merge request changes' do
......
...@@ -145,7 +145,7 @@ module API ...@@ -145,7 +145,7 @@ module API
given snowplow_enabled: ->(val) { val } do given snowplow_enabled: ->(val) { val } do
requires :snowplow_collector_hostname, type: String, desc: 'The Snowplow collector hostname' requires :snowplow_collector_hostname, type: String, desc: 'The Snowplow collector hostname'
optional :snowplow_cookie_domain, type: String, desc: 'The Snowplow cookie domain' optional :snowplow_cookie_domain, type: String, desc: 'The Snowplow cookie domain'
optional :snowplow_site_id, type: String, desc: 'The Snowplow site name / application ic' optional :snowplow_app_id, type: String, desc: 'The Snowplow site name / application id'
end end
optional :pendo_enabled, type: Grape::API::Boolean, desc: 'Enable Pendo tracking' optional :pendo_enabled, type: Grape::API::Boolean, desc: 'Enable Pendo tracking'
given pendo_enabled: ->(val) { val } do given pendo_enabled: ->(val) { val } do
......
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
##
# Entry that represents the interrutible value.
#
class Boolean < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, boolean: true
end
end
end
end
end
end
...@@ -14,7 +14,7 @@ module Gitlab ...@@ -14,7 +14,7 @@ module Gitlab
include ::Gitlab::Config::Entry::Inheritable include ::Gitlab::Config::Entry::Inheritable
ALLOWED_KEYS = %i[before_script image services ALLOWED_KEYS = %i[before_script image services
after_script cache].freeze after_script cache interruptible].freeze
validations do validations do
validates :config, allowed_keys: ALLOWED_KEYS validates :config, allowed_keys: ALLOWED_KEYS
...@@ -40,7 +40,11 @@ module Gitlab ...@@ -40,7 +40,11 @@ module Gitlab
description: 'Configure caching between build jobs.', description: 'Configure caching between build jobs.',
inherit: true inherit: true
helpers :before_script, :image, :services, :after_script, :cache entry :interruptible, Entry::Boolean,
description: 'Set jobs interruptible default value.',
inherit: false
helpers :before_script, :image, :services, :after_script, :cache, :interruptible
private private
......
...@@ -38,7 +38,6 @@ module Gitlab ...@@ -38,7 +38,6 @@ module Gitlab
with_options allow_nil: true do with_options allow_nil: true do
validates :tags, array_of_strings: true validates :tags, array_of_strings: true
validates :allow_failure, boolean: true validates :allow_failure, boolean: true
validates :interruptible, boolean: true
validates :parallel, numericality: { only_integer: true, validates :parallel, numericality: { only_integer: true,
greater_than_or_equal_to: 2, greater_than_or_equal_to: 2,
less_than_or_equal_to: 50 } less_than_or_equal_to: 50 }
...@@ -100,6 +99,10 @@ module Gitlab ...@@ -100,6 +99,10 @@ module Gitlab
description: 'Services that will be used to execute this job.', description: 'Services that will be used to execute this job.',
inherit: true inherit: true
entry :interruptible, Entry::Boolean,
description: 'Set jobs interruptible value.',
inherit: true
entry :only, Entry::Policy, entry :only, Entry::Policy,
description: 'Refs policy this job will be executed for.', description: 'Refs policy this job will be executed for.',
default: Entry::Policy::DEFAULT_ONLY, default: Entry::Policy::DEFAULT_ONLY,
......
# frozen_string_literal: true
module Gitlab
module ErrorTracking
class DetailedError
include ActiveModel::Model
attr_accessor :count,
:culprit,
:external_base_url,
:external_url,
:first_release_last_commit,
:first_release_short_version,
:first_seen,
:frequency,
:id,
:last_release_last_commit,
:last_release_short_version,
:last_seen,
:message,
:project_id,
:project_name,
:project_slug,
:short_id,
:status,
:title,
:type,
:user_count
end
end
end
# frozen_string_literal: true
module Gitlab
module ErrorTracking
class ErrorEvent
include ActiveModel::Model
attr_accessor :issue_id, :date_received, :stack_trace_entries
end
end
end
...@@ -45,7 +45,7 @@ module Gitlab ...@@ -45,7 +45,7 @@ module Gitlab
namespace: SNOWPLOW_NAMESPACE, namespace: SNOWPLOW_NAMESPACE,
hostname: Gitlab::CurrentSettings.snowplow_collector_hostname, hostname: Gitlab::CurrentSettings.snowplow_collector_hostname,
cookie_domain: Gitlab::CurrentSettings.snowplow_cookie_domain, cookie_domain: Gitlab::CurrentSettings.snowplow_cookie_domain,
app_id: Gitlab::CurrentSettings.snowplow_site_id, app_id: Gitlab::CurrentSettings.snowplow_app_id,
form_tracking: additional_features, form_tracking: additional_features,
link_click_tracking: additional_features, link_click_tracking: additional_features,
iglu_registry_url: Gitlab::CurrentSettings.snowplow_iglu_registry_url iglu_registry_url: Gitlab::CurrentSettings.snowplow_iglu_registry_url
...@@ -59,7 +59,7 @@ module Gitlab ...@@ -59,7 +59,7 @@ module Gitlab
SnowplowTracker::AsyncEmitter.new(Gitlab::CurrentSettings.snowplow_collector_hostname, protocol: 'https'), SnowplowTracker::AsyncEmitter.new(Gitlab::CurrentSettings.snowplow_collector_hostname, protocol: 'https'),
SnowplowTracker::Subject.new, SnowplowTracker::Subject.new,
SNOWPLOW_NAMESPACE, SNOWPLOW_NAMESPACE,
Gitlab::CurrentSettings.snowplow_site_id Gitlab::CurrentSettings.snowplow_app_id
) )
end end
end end
......
...@@ -12,6 +12,18 @@ module Sentry ...@@ -12,6 +12,18 @@ module Sentry
@token = token @token = token
end end
def issue_details(issue_id:)
issue = get_issue(issue_id: issue_id)
map_to_detailed_error(issue)
end
def issue_latest_event(issue_id:)
latest_event = get_issue_latest_event(issue_id: issue_id)
map_to_event(latest_event)
end
def list_issues(issue_status:, limit:) def list_issues(issue_status:, limit:)
issues = get_issues(issue_status: issue_status, limit: limit) issues = get_issues(issue_status: issue_status, limit: limit)
...@@ -61,6 +73,14 @@ module Sentry ...@@ -61,6 +73,14 @@ module Sentry
}) })
end end
def get_issue(issue_id:)
http_get(issue_api_url(issue_id))
end
def get_issue_latest_event(issue_id:)
http_get(issue_latest_event_api_url(issue_id))
end
def get_projects def get_projects
http_get(projects_api_url) http_get(projects_api_url)
end end
...@@ -102,6 +122,20 @@ module Sentry ...@@ -102,6 +122,20 @@ module Sentry
projects_url projects_url
end end
def issue_api_url(issue_id)
issue_url = URI(@url)
issue_url.path = "/api/0/issues/#{issue_id}/"
issue_url
end
def issue_latest_event_api_url(issue_id)
latest_event_url = URI(@url)
latest_event_url.path = "/api/0/issues/#{issue_id}/events/latest/"
latest_event_url
end
def issues_api_url def issues_api_url
issues_url = URI(@url + '/issues/') issues_url = URI(@url + '/issues/')
issues_url.path.squeeze!('/') issues_url.path.squeeze!('/')
...@@ -119,38 +153,87 @@ module Sentry ...@@ -119,38 +153,87 @@ module Sentry
def issue_url(id) def issue_url(id)
issues_url = @url + "/issues/#{id}" issues_url = @url + "/issues/#{id}"
issues_url = ErrorTracking::ProjectErrorTrackingSetting.extract_sentry_external_url(issues_url)
uri = URI(issues_url) parse_sentry_url(issues_url)
end
def project_url
parse_sentry_url(@url)
end
def parse_sentry_url(api_url)
url = ErrorTracking::ProjectErrorTrackingSetting.extract_sentry_external_url(api_url)
uri = URI(url)
uri.path.squeeze!('/') uri.path.squeeze!('/')
# Remove trailing spaces
uri = uri.to_s.gsub(/\/\z/, '')
uri.to_s uri
end end
def map_to_error(issue) def map_to_event(event)
id = issue.fetch('id') stack_trace = parse_stack_trace(event)
Gitlab::ErrorTracking::ErrorEvent.new(
issue_id: event.dig('groupID'),
date_received: event.dig('dateReceived'),
stack_trace_entries: stack_trace
)
end
count = issue.fetch('count', nil) def parse_stack_trace(event)
exception_entry = event.dig('entries')&.detect { |h| h['type'] == 'exception' }
return unless exception_entry
frequency = issue.dig('stats', '24h') exception_values = exception_entry.dig('data', 'values')
message = issue.dig('metadata', 'value') stack_trace_entry = exception_values&.detect { |h| h['stacktrace'].present? }
return unless stack_trace_entry
external_url = issue_url(id) stack_trace_entry.dig('stacktrace', 'frames')
end
def map_to_detailed_error(issue)
Gitlab::ErrorTracking::DetailedError.new(
id: issue.fetch('id'),
first_seen: issue.fetch('firstSeen', nil),
last_seen: issue.fetch('lastSeen', nil),
title: issue.fetch('title', nil),
type: issue.fetch('type', nil),
user_count: issue.fetch('userCount', nil),
count: issue.fetch('count', nil),
message: issue.dig('metadata', 'value'),
culprit: issue.fetch('culprit', nil),
external_url: issue_url(issue.fetch('id')),
external_base_url: project_url,
short_id: issue.fetch('shortId', nil),
status: issue.fetch('status', nil),
frequency: issue.dig('stats', '24h'),
project_id: issue.dig('project', 'id'),
project_name: issue.dig('project', 'name'),
project_slug: issue.dig('project', 'slug'),
first_release_last_commit: issue.dig('firstRelease', 'lastCommit'),
last_release_last_commit: issue.dig('lastRelease', 'lastCommit'),
first_release_short_version: issue.dig('firstRelease', 'shortVersion'),
last_release_short_version: issue.dig('lastRelease', 'shortVersion')
)
end
def map_to_error(issue)
Gitlab::ErrorTracking::Error.new( Gitlab::ErrorTracking::Error.new(
id: id, id: issue.fetch('id'),
first_seen: issue.fetch('firstSeen', nil), first_seen: issue.fetch('firstSeen', nil),
last_seen: issue.fetch('lastSeen', nil), last_seen: issue.fetch('lastSeen', nil),
title: issue.fetch('title', nil), title: issue.fetch('title', nil),
type: issue.fetch('type', nil), type: issue.fetch('type', nil),
user_count: issue.fetch('userCount', nil), user_count: issue.fetch('userCount', nil),
count: count, count: issue.fetch('count', nil),
message: message, message: issue.dig('metadata', 'value'),
culprit: issue.fetch('culprit', nil), culprit: issue.fetch('culprit', nil),
external_url: external_url, external_url: issue_url(issue.fetch('id')),
short_id: issue.fetch('shortId', nil), short_id: issue.fetch('shortId', nil),
status: issue.fetch('status', nil), status: issue.fetch('status', nil),
frequency: frequency, frequency: issue.dig('stats', '24h'),
project_id: issue.dig('project', 'id'), project_id: issue.dig('project', 'id'),
project_name: issue.dig('project', 'name'), project_name: issue.dig('project', 'name'),
project_slug: issue.dig('project', 'slug') project_slug: issue.dig('project', 'slug')
......
...@@ -1798,6 +1798,9 @@ msgstr "" ...@@ -1798,6 +1798,9 @@ msgstr ""
msgid "Any user" msgid "Any user"
msgstr "" msgstr ""
msgid "App ID"
msgstr ""
msgid "Appearance" msgid "Appearance"
msgstr "" msgstr ""
...@@ -6406,12 +6409,18 @@ msgstr "" ...@@ -6406,12 +6409,18 @@ msgstr ""
msgid "Environments|Job" msgid "Environments|Job"
msgstr "" msgstr ""
msgid "Environments|Learn about environments"
msgstr ""
msgid "Environments|Learn more about stopping environments" msgid "Environments|Learn more about stopping environments"
msgstr "" msgstr ""
msgid "Environments|New environment" msgid "Environments|New environment"
msgstr "" msgstr ""
msgid "Environments|No deployed environments"
msgstr ""
msgid "Environments|No deployments yet" msgid "Environments|No deployments yet"
msgstr "" msgstr ""
...@@ -6580,6 +6589,9 @@ msgstr "" ...@@ -6580,6 +6589,9 @@ msgstr ""
msgid "Error" msgid "Error"
msgstr "" msgstr ""
msgid "Error Details"
msgstr ""
msgid "Error Tracking" msgid "Error Tracking"
msgstr "" msgstr ""
...@@ -10177,6 +10189,9 @@ msgstr "" ...@@ -10177,6 +10189,9 @@ msgstr ""
msgid "Logs" msgid "Logs"
msgstr "" msgstr ""
msgid "Logs|To see the pod logs, deploy your code to an environment."
msgstr ""
msgid "MD5" msgid "MD5"
msgstr "" msgstr ""
...@@ -10678,9 +10693,6 @@ msgstr "" ...@@ -10678,9 +10693,6 @@ msgstr ""
msgid "Metrics|Label of the y-axis (usually the unit). The x-axis always represents time." msgid "Metrics|Label of the y-axis (usually the unit). The x-axis always represents time."
msgstr "" msgstr ""
msgid "Metrics|Learn about environments"
msgstr ""
msgid "Metrics|Legend label (optional)" msgid "Metrics|Legend label (optional)"
msgstr "" msgstr ""
...@@ -10696,9 +10708,6 @@ msgstr "" ...@@ -10696,9 +10708,6 @@ msgstr ""
msgid "Metrics|New metric" msgid "Metrics|New metric"
msgstr "" msgstr ""
msgid "Metrics|No deployed environments"
msgstr ""
msgid "Metrics|PromQL query is valid" msgid "Metrics|PromQL query is valid"
msgstr "" msgstr ""
...@@ -12355,6 +12364,9 @@ msgstr "" ...@@ -12355,6 +12364,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will." msgid "Please wait while we import the repository for you. Refresh at will."
msgstr "" msgstr ""
msgid "Pod logs"
msgstr ""
msgid "Pod not found" msgid "Pod not found"
msgstr "" msgstr ""
...@@ -15599,9 +15611,6 @@ msgstr "" ...@@ -15599,9 +15611,6 @@ msgstr ""
msgid "Single or combined queries" msgid "Single or combined queries"
msgstr "" msgstr ""
msgid "Site ID"
msgstr ""
msgid "Size" msgid "Size"
msgstr "" msgstr ""
......
...@@ -90,14 +90,6 @@ describe ApplicationController do ...@@ -90,14 +90,6 @@ describe ApplicationController do
let(:format) { :html } let(:format) { :html }
it_behaves_like 'setting gon variables' it_behaves_like 'setting gon variables'
context 'for peek requests' do
before do
request.path = '/-/peek'
end
it_behaves_like 'not setting gon variables'
end
end end
context 'with json format' do context 'with json format' do
...@@ -105,6 +97,12 @@ describe ApplicationController do ...@@ -105,6 +97,12 @@ describe ApplicationController do
it_behaves_like 'not setting gon variables' it_behaves_like 'not setting gon variables'
end end
context 'with atom format' do
let(:format) { :atom }
it_behaves_like 'not setting gon variables'
end
end end
describe 'session expiration' do describe 'session expiration' do
......
...@@ -330,11 +330,11 @@ describe Projects::EnvironmentsController do ...@@ -330,11 +330,11 @@ describe Projects::EnvironmentsController do
expect(response).to redirect_to(environment_metrics_path(environment)) expect(response).to redirect_to(environment_metrics_path(environment))
end end
it 'redirects to empty page if no environment exists' do it 'redirects to empty metrics page if no environment exists' do
get :metrics_redirect, params: { namespace_id: project.namespace, project_id: project } get :metrics_redirect, params: { namespace_id: project.namespace, project_id: project }
expect(response).to be_ok expect(response).to be_ok
expect(response).to render_template 'empty' expect(response).to render_template 'empty_metrics'
end end
end end
......
...@@ -46,17 +46,6 @@ describe Projects::ErrorTrackingController do ...@@ -46,17 +46,6 @@ describe Projects::ErrorTrackingController do
end end
describe 'format json' do describe 'format json' do
shared_examples 'no data' do
it 'returns no data' do
get :index, params: project_params(format: :json)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('error_tracking/index')
expect(json_response['external_url']).to be_nil
expect(json_response['errors']).to eq([])
end
end
let(:list_issues_service) { spy(:list_issues_service) } let(:list_issues_service) { spy(:list_issues_service) }
let(:external_url) { 'http://example.com' } let(:external_url) { 'http://example.com' }
...@@ -66,6 +55,19 @@ describe Projects::ErrorTrackingController do ...@@ -66,6 +55,19 @@ describe Projects::ErrorTrackingController do
.and_return(list_issues_service) .and_return(list_issues_service)
end end
context 'no data' do
before do
expect(list_issues_service).to receive(:execute)
.and_return(status: :error, http_status: :no_content)
end
it 'returns no data' do
get :index, params: project_params(format: :json)
expect(response).to have_gitlab_http_status(:no_content)
end
end
context 'service result is successful' do context 'service result is successful' do
before do before do
expect(list_issues_service).to receive(:execute) expect(list_issues_service).to receive(:execute)
...@@ -232,8 +234,186 @@ describe Projects::ErrorTrackingController do ...@@ -232,8 +234,186 @@ describe Projects::ErrorTrackingController do
end end
end end
describe 'GET #issue_details' do
let_it_be(:issue_id) { 1234 }
let(:issue_details_service) { spy(:issue_details_service) }
let(:permitted_params) do
ActionController::Parameters.new(
{ issue_id: issue_id.to_s }
).permit!
end
before do
expect(ErrorTracking::IssueDetailsService)
.to receive(:new).with(project, user, permitted_params)
.and_return(issue_details_service)
end
describe 'format json' do
context 'no data' do
before do
expect(issue_details_service).to receive(:execute)
.and_return(status: :error, http_status: :no_content)
end
it 'returns no data' do
get :details, params: issue_params(issue_id: issue_id, format: :json)
expect(response).to have_gitlab_http_status(:no_content)
end
end
context 'service result is successful' do
before do
expect(issue_details_service).to receive(:execute)
.and_return(status: :success, issue: error)
end
let(:error) { build(:detailed_error_tracking_error) }
it 'returns an error' do
get :details, params: issue_params(issue_id: issue_id, format: :json)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('error_tracking/issue_detailed')
expect(json_response['error']).to eq(error.as_json)
end
end
context 'service result is erroneous' do
let(:error_message) { 'error message' }
context 'without http_status' do
before do
expect(issue_details_service).to receive(:execute)
.and_return(status: :error, message: error_message)
end
it 'returns 400 with message' do
get :details, params: issue_params(issue_id: issue_id, format: :json)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq(error_message)
end
end
context 'with explicit http_status' do
let(:http_status) { :no_content }
before do
expect(issue_details_service).to receive(:execute).and_return(
status: :error,
message: error_message,
http_status: http_status
)
end
it 'returns http_status with message' do
get :details, params: issue_params(issue_id: issue_id, format: :json)
expect(response).to have_gitlab_http_status(http_status)
expect(json_response['message']).to eq(error_message)
end
end
end
end
end
describe 'GET #stack_trace' do
let_it_be(:issue_id) { 1234 }
let(:issue_stack_trace_service) { spy(:issue_stack_trace_service) }
let(:permitted_params) do
ActionController::Parameters.new(
{ issue_id: issue_id.to_s }
).permit!
end
before do
expect(ErrorTracking::IssueLatestEventService)
.to receive(:new).with(project, user, permitted_params)
.and_return(issue_stack_trace_service)
end
describe 'format json' do
context 'awaiting data' do
before do
expect(issue_stack_trace_service).to receive(:execute)
.and_return(status: :error, http_status: :no_content)
end
it 'returns no data' do
get :stack_trace, params: issue_params(issue_id: issue_id, format: :json)
expect(response).to have_gitlab_http_status(:no_content)
end
end
context 'service result is successful' do
before do
expect(issue_stack_trace_service).to receive(:execute)
.and_return(status: :success, latest_event: error_event)
end
let(:error_event) { build(:error_tracking_error_event) }
it 'returns an error' do
get :stack_trace, params: issue_params(issue_id: issue_id, format: :json)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('error_tracking/issue_stack_trace')
expect(json_response['error']).to eq(error_event.as_json)
end
end
context 'service result is erroneous' do
let(:error_message) { 'error message' }
context 'without http_status' do
before do
expect(issue_stack_trace_service).to receive(:execute)
.and_return(status: :error, message: error_message)
end
it 'returns 400 with message' do
get :stack_trace, params: issue_params(issue_id: issue_id, format: :json)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq(error_message)
end
end
context 'with explicit http_status' do
let(:http_status) { :no_content }
before do
expect(issue_stack_trace_service).to receive(:execute).and_return(
status: :error,
message: error_message,
http_status: http_status
)
end
it 'returns http_status with message' do
get :stack_trace, params: issue_params(issue_id: issue_id, format: :json)
expect(response).to have_gitlab_http_status(http_status)
expect(json_response['message']).to eq(error_message)
end
end
end
end
end
private private
def issue_params(opts = {})
project_params.reverse_merge(opts)
end
def project_params(opts = {}) def project_params(opts = {})
opts.reverse_merge(namespace_id: project.namespace, project_id: project) opts.reverse_merge(namespace_id: project.namespace, project_id: project)
end end
......
...@@ -228,10 +228,10 @@ describe UploadsController do ...@@ -228,10 +228,10 @@ describe UploadsController do
user.block user.block
end end
it "redirects to the sign in page" do it "responds with status 401" do
get :show, params: { model: "user", mounted_as: "avatar", id: user.id, filename: "dk.png" } get :show, params: { model: "user", mounted_as: "avatar", id: user.id, filename: "dk.png" }
expect(response).to redirect_to(new_user_session_path) expect(response).to have_gitlab_http_status(401)
end end
end end
...@@ -320,10 +320,10 @@ describe UploadsController do ...@@ -320,10 +320,10 @@ describe UploadsController do
end end
context "when not signed in" do context "when not signed in" do
it "redirects to the sign in page" do it "responds with status 401" do
get :show, params: { model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" } get :show, params: { model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" }
expect(response).to redirect_to(new_user_session_path) expect(response).to have_gitlab_http_status(401)
end end
end end
...@@ -343,10 +343,10 @@ describe UploadsController do ...@@ -343,10 +343,10 @@ describe UploadsController do
project.add_maintainer(user) project.add_maintainer(user)
end end
it "redirects to the sign in page" do it "responds with status 401" do
get :show, params: { model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" } get :show, params: { model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" }
expect(response).to redirect_to(new_user_session_path) expect(response).to have_gitlab_http_status(401)
end end
end end
...@@ -439,10 +439,10 @@ describe UploadsController do ...@@ -439,10 +439,10 @@ describe UploadsController do
user.block user.block
end end
it "redirects to the sign in page" do it "responds with status 401" do
get :show, params: { model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png" } get :show, params: { model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png" }
expect(response).to redirect_to(new_user_session_path) expect(response).to have_gitlab_http_status(401)
end end
end end
...@@ -526,10 +526,10 @@ describe UploadsController do ...@@ -526,10 +526,10 @@ describe UploadsController do
end end
context "when not signed in" do context "when not signed in" do
it "redirects to the sign in page" do it "responds with status 401" do
get :show, params: { model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" } get :show, params: { model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" }
expect(response).to redirect_to(new_user_session_path) expect(response).to have_gitlab_http_status(401)
end end
end end
...@@ -549,10 +549,10 @@ describe UploadsController do ...@@ -549,10 +549,10 @@ describe UploadsController do
project.add_maintainer(user) project.add_maintainer(user)
end end
it "redirects to the sign in page" do it "responds with status 401" do
get :show, params: { model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" } get :show, params: { model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" }
expect(response).to redirect_to(new_user_session_path) expect(response).to have_gitlab_http_status(401)
end end
end end
......
...@@ -13,7 +13,7 @@ describe 'Database schema' do ...@@ -13,7 +13,7 @@ describe 'Database schema' do
# EE: edit the ee/spec/db/schema_support.rb # EE: edit the ee/spec/db/schema_support.rb
IGNORED_FK_COLUMNS = { IGNORED_FK_COLUMNS = {
abuse_reports: %w[reporter_id user_id], abuse_reports: %w[reporter_id user_id],
application_settings: %w[performance_bar_allowed_group_id slack_app_id snowplow_site_id eks_account_id eks_access_key_id], application_settings: %w[performance_bar_allowed_group_id slack_app_id snowplow_app_id eks_account_id eks_access_key_id],
approvers: %w[target_id user_id], approvers: %w[target_id user_id],
approvals: %w[user_id], approvals: %w[user_id],
approver_groups: %w[target_id], approver_groups: %w[target_id],
......
# frozen_string_literal: true
FactoryBot.define do
factory :detailed_error_tracking_error, class: Gitlab::ErrorTracking::DetailedError do
id { 'id' }
title { 'title' }
type { 'error' }
user_count { 1 }
count { 2 }
first_seen { Time.now }
last_seen { Time.now }
message { 'message' }
culprit { 'culprit' }
external_url { 'http://example.com/id' }
external_base_url { 'http://example.com' }
project_id { 'project1' }
project_name { 'project name' }
project_slug { 'project_name' }
short_id { 'ID' }
status { 'unresolved' }
frequency { [] }
first_release_last_commit { '68c914da9' }
last_release_last_commit { '9ad419c86' }
first_release_short_version { 'abc123' }
last_release_short_version { 'abc123' }
skip_create
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :error_tracking_error_event, class: Gitlab::ErrorTracking::ErrorEvent do
issue_id { 'id' }
date_received { Time.now.iso8601 }
stack_trace_entries do
{
'stacktrace' =>
{
'frames' => [{ 'file' => 'test.rb' }]
}
}
end
skip_create
end
end
...@@ -20,7 +20,7 @@ describe 'Projects > Members > Member leaves project' do ...@@ -20,7 +20,7 @@ describe 'Projects > Members > Member leaves project' do
expect(project.users.exists?(user.id)).to be_falsey expect(project.users.exists?(user.id)).to be_falsey
end end
it 'user leaves project by url param', :js do it 'user leaves project by url param', :js, :quarantine do
visit project_path(project, leave: 1) visit project_path(project, leave: 1)
page.accept_confirm page.accept_confirm
......
...@@ -4,7 +4,14 @@ ...@@ -4,7 +4,14 @@
"external_url", "external_url",
"last_seen", "last_seen",
"message", "message",
"type" "type",
"title",
"project_id",
"project_name",
"project_slug",
"short_id",
"status",
"frequency"
], ],
"properties" : { "properties" : {
"id": { "type": "string"}, "id": { "type": "string"},
...@@ -15,7 +22,14 @@ ...@@ -15,7 +22,14 @@
"culprit": { "type": "string" }, "culprit": { "type": "string" },
"count": { "type": "integer"}, "count": { "type": "integer"},
"external_url": { "type": "string" }, "external_url": { "type": "string" },
"user_count": { "type": "integer"} "user_count": { "type": "integer"},
"title": { "type": "string"},
"project_id": { "type": "string"},
"project_name": { "type": "string"},
"project_slug": { "type": "string"},
"short_id": { "type": "string"},
"status": { "type": "string"},
"frequency": { "type": "array"}
}, },
"additionalProperties": true "additionalProperties": false
} }
{
"type": "object",
"required" : [
"external_url",
"external_base_url",
"last_seen",
"message",
"type",
"title",
"project_id",
"project_name",
"project_slug",
"short_id",
"status",
"frequency",
"first_release_last_commit",
"last_release_last_commit",
"first_release_short_version",
"last_release_short_version"
],
"properties" : {
"id": { "type": "string"},
"first_seen": { "type": "string", "format": "date-time" },
"last_seen": { "type": "string", "format": "date-time" },
"type": { "type": "string" },
"message": { "type": "string" },
"culprit": { "type": "string" },
"count": { "type": "integer"},
"external_url": { "type": "string" },
"external_base_url": { "type": "string" },
"user_count": { "type": "integer"},
"title": { "type": "string"},
"project_id": { "type": "string"},
"project_name": { "type": "string"},
"project_slug": { "type": "string"},
"short_id": { "type": "string"},
"status": { "type": "string"},
"frequency": { "type": "array"},
"first_release_last_commit": { "type": ["string", "null"] },
"last_release_last_commit": { "type": ["string", "null"] },
"first_release_short_version": { "type": ["string", "null"] },
"last_release_short_version": { "type": ["string", "null"] }
},
"additionalProperties": false
}
{
"type": "object",
"required": [
"issue_id",
"stack_trace_entries",
"date_received"
],
"properties": {
"issue_id": { "type": ["string", "integer"] },
"stack_trace_entries": { "type": "object" },
"date_received": { "type": "string" }
},
"additionalProperties": false
}
{
"type": "object",
"required": [
"error"
],
"properties": {
"error": { "$ref": "error_detailed.json" }
},
"additionalProperties": false
}
{
"type": "object",
"required": [
"error"
],
"properties": {
"error": { "$ref": "error_stack_trace.json" }
},
"additionalProperties": false
}
...@@ -38,7 +38,7 @@ describe ApplicationSettingsHelper do ...@@ -38,7 +38,7 @@ describe ApplicationSettingsHelper do
it_behaves_like 'when HTTP protocol is in use', 'http' it_behaves_like 'when HTTP protocol is in use', 'http'
context 'with tracking parameters' do context 'with tracking parameters' do
it { expect(visible_attributes).to include(*%i(snowplow_collector_hostname snowplow_cookie_domain snowplow_enabled snowplow_site_id)) } it { expect(visible_attributes).to include(*%i(snowplow_collector_hostname snowplow_cookie_domain snowplow_enabled snowplow_app_id)) }
it { expect(visible_attributes).to include(*%i(pendo_enabled pendo_url)) } it { expect(visible_attributes).to include(*%i(pendo_enabled pendo_url)) }
end end
......
...@@ -26,7 +26,7 @@ describe Gitlab::Ci::Config::Entry::Default do ...@@ -26,7 +26,7 @@ describe Gitlab::Ci::Config::Entry::Default do
it 'contains the expected node names' do it 'contains the expected node names' do
expect(described_class.nodes.keys) expect(described_class.nodes.keys)
.to match_array(%i[before_script image services .to match_array(%i[before_script image services
after_script cache]) after_script cache interruptible])
end end
end end
end end
......
...@@ -24,7 +24,7 @@ describe Gitlab::Ci::Config::Entry::Job do ...@@ -24,7 +24,7 @@ describe Gitlab::Ci::Config::Entry::Job do
let(:result) do let(:result) do
%i[before_script script stage type after_script cache %i[before_script script stage type after_script cache
image services only except rules needs variables artifacts image services only except rules needs variables artifacts
environment coverage retry] environment coverage retry interruptible]
end end
it { is_expected.to match_array result } it { is_expected.to match_array result }
......
...@@ -108,6 +108,25 @@ module Gitlab ...@@ -108,6 +108,25 @@ module Gitlab
it { expect(subject[:interruptible]).to be_falsy } it { expect(subject[:interruptible]).to be_falsy }
end end
it "returns interruptible when overridden for job" do
config = YAML.dump({ default: { interruptible: true },
rspec: { script: "rspec" } })
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test",
stage_idx: 2,
name: "rspec",
options: { script: ["rspec"] },
interruptible: true,
allow_failure: false,
when: "on_success",
yaml_variables: []
})
end
end end
describe 'retry entry' do describe 'retry entry' do
......
...@@ -8,7 +8,7 @@ describe Gitlab::Tracking do ...@@ -8,7 +8,7 @@ describe Gitlab::Tracking do
stub_application_setting(snowplow_enabled: true) stub_application_setting(snowplow_enabled: true)
stub_application_setting(snowplow_collector_hostname: 'gitfoo.com') stub_application_setting(snowplow_collector_hostname: 'gitfoo.com')
stub_application_setting(snowplow_cookie_domain: '.gitfoo.com') stub_application_setting(snowplow_cookie_domain: '.gitfoo.com')
stub_application_setting(snowplow_site_id: '_abc123_') stub_application_setting(snowplow_app_id: '_abc123_')
stub_application_setting(snowplow_iglu_registry_url: 'https://example.org') stub_application_setting(snowplow_iglu_registry_url: 'https://example.org')
end end
......
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20191004081520_fill_productivity_analytics_start_date.rb')
describe FillProductivityAnalyticsStartDate, :migration do
let(:settings_table) { table('application_settings') }
let(:metrics_table) { table('merge_request_metrics') }
before do
settings_table.create!
end
context 'with NO productivity analytics data available' do
it 'sets start_date to NOW' do
expect { migrate! }.to change {
settings_table.first&.productivity_analytics_start_date
}.to(be_like_time(Time.now))
end
end
context 'with productivity analytics data available' do
before do
ActiveRecord::Base.transaction do
ActiveRecord::Base.connection.execute('ALTER TABLE merge_request_metrics DISABLE TRIGGER ALL')
metrics_table.create!(merged_at: Time.parse('2019-09-09'), commits_count: nil, merge_request_id: 3)
metrics_table.create!(merged_at: Time.parse('2019-10-10'), commits_count: 5, merge_request_id: 1)
metrics_table.create!(merged_at: Time.parse('2019-11-11'), commits_count: 10, merge_request_id: 2)
ActiveRecord::Base.connection.execute('ALTER TABLE merge_request_metrics ENABLE TRIGGER ALL')
end
end
it 'set start_date to earliest merged_at value with PA data available' do
expect { migrate! }.to change {
settings_table.first&.productivity_analytics_start_date
}.to(be_like_time(Time.parse('2019-10-10')))
end
end
end
...@@ -178,7 +178,7 @@ describe API::Settings, 'Settings' do ...@@ -178,7 +178,7 @@ describe API::Settings, 'Settings' do
snowplow_collector_hostname: "snowplow.example.com", snowplow_collector_hostname: "snowplow.example.com",
snowplow_cookie_domain: ".example.com", snowplow_cookie_domain: ".example.com",
snowplow_enabled: true, snowplow_enabled: true,
snowplow_site_id: "site_id", snowplow_app_id: "app_id",
snowplow_iglu_registry_url: 'https://example.com' snowplow_iglu_registry_url: 'https://example.com'
} }
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Loading a user avatar' do
let(:user) { create(:user, :with_avatar) }
context 'when logged in' do
# The exact query count will vary depending on the 2FA settings of the
# instance, group, and user. Removing those extra 2FA queries in this case
# may not be a good idea, so we just set up the ideal case.
before do
stub_application_setting(require_two_factor_authentication: true)
login_as(create(:user, :two_factor))
end
# One each for: current user, avatar user, and upload record
it 'only performs three SQL queries' do
get user.avatar_url # Skip queries on first application load
expect(response).to have_gitlab_http_status(200)
expect { get user.avatar_url }.not_to exceed_query_limit(3)
end
end
context 'when logged out' do
# One each for avatar user and upload record
it 'only performs two SQL queries' do
get user.avatar_url # Skip queries on first application load
expect(response).to have_gitlab_http_status(200)
expect { get user.avatar_url }.not_to exceed_query_limit(2)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ErrorTracking::IssueDetailsService do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
let(:token) { 'test-token' }
let(:result) { subject.execute }
let(:error_tracking_setting) do
create(:project_error_tracking_setting, api_url: sentry_url, token: token, project: project)
end
subject { described_class.new(project, user) }
before do
expect(project).to receive(:error_tracking_setting).at_least(:once).and_return(error_tracking_setting)
project.add_reporter(user)
end
describe '#execute' do
context 'with authorized user' do
context 'when issue_details returns a detailed error' do
let(:detailed_error) { build(:detailed_error_tracking_error) }
before do
expect(error_tracking_setting)
.to receive(:issue_details).and_return(issue: detailed_error)
end
it 'returns the detailed error' do
expect(result).to eq(status: :success, issue: detailed_error)
end
end
include_examples 'error tracking service data not ready', :issue_details
include_examples 'error tracking service sentry error handling', :issue_details
include_examples 'error tracking service http status handling', :issue_details
end
include_examples 'error tracking service unauthorized user'
include_examples 'error tracking service disabled'
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ErrorTracking::IssueLatestEventService do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
let(:token) { 'test-token' }
let(:result) { subject.execute }
let(:error_tracking_setting) do
create(:project_error_tracking_setting, api_url: sentry_url, token: token, project: project)
end
subject { described_class.new(project, user) }
before do
expect(project).to receive(:error_tracking_setting).at_least(:once).and_return(error_tracking_setting)
project.add_reporter(user)
end
describe '#execute' do
context 'with authorized user' do
context 'when issue_latest_event returns an error event' do
let(:error_event) { build(:error_tracking_error_event) }
before do
expect(error_tracking_setting)
.to receive(:issue_latest_event).and_return(latest_event: error_event)
end
it 'returns the error event' do
expect(result).to eq(status: :success, latest_event: error_event)
end
end
include_examples 'error tracking service data not ready', :issue_latest_event
include_examples 'error tracking service sentry error handling', :issue_latest_event
include_examples 'error tracking service http status handling', :issue_latest_event
end
include_examples 'error tracking service unauthorized user'
include_examples 'error tracking service disabled'
end
end
...@@ -37,93 +37,12 @@ describe ErrorTracking::ListIssuesService do ...@@ -37,93 +37,12 @@ describe ErrorTracking::ListIssuesService do
end end
end end
context 'when list_sentry_issues returns nil' do include_examples 'error tracking service data not ready', :list_sentry_issues
before do include_examples 'error tracking service sentry error handling', :list_sentry_issues
expect(error_tracking_setting) include_examples 'error tracking service http status handling', :list_sentry_issues
.to receive(:list_sentry_issues).and_return(nil)
end
it 'result is not ready' do
expect(result).to eq(
status: :error, http_status: :no_content, message: 'Not ready. Try again later')
end
end
context 'when list_sentry_issues returns error' do
before do
allow(error_tracking_setting)
.to receive(:list_sentry_issues)
.and_return(
error: 'Sentry response status code: 401',
error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE
)
end
it 'returns the error' do
expect(result).to eq(
status: :error,
http_status: :bad_request,
message: 'Sentry response status code: 401'
)
end
end
context 'when list_sentry_issues returns error with http_status' do
before do
allow(error_tracking_setting)
.to receive(:list_sentry_issues)
.and_return(
error: 'Sentry API response is missing keys. key not found: "id"',
error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_MISSING_KEYS
)
end
it 'returns the error with correct http_status' do
expect(result).to eq(
status: :error,
http_status: :internal_server_error,
message: 'Sentry API response is missing keys. key not found: "id"'
)
end
end
end end
context 'with unauthorized user' do include_examples 'error tracking service unauthorized user'
let(:unauthorized_user) { create(:user) } include_examples 'error tracking service disabled'
subject { described_class.new(project, unauthorized_user) }
it 'returns error' do
result = subject.execute
expect(result).to include(
status: :error,
message: 'Access denied',
http_status: :unauthorized
)
end
end
context 'with error tracking disabled' do
before do
error_tracking_setting.enabled = false
end
it 'raises error' do
result = subject.execute
expect(result).to include(status: :error, message: 'Error Tracking is not enabled')
end
end
end
describe '#sentry_external_url' do
let(:external_url) { 'https://sentrytest.gitlab.com/sentry-org/sentry-project' }
it 'calls ErrorTracking::ProjectErrorTrackingSetting' do
expect(error_tracking_setting).to receive(:sentry_external_url).and_call_original
subject.external_url
end
end end
end end
...@@ -127,7 +127,7 @@ describe ErrorTracking::ListProjectsService do ...@@ -127,7 +127,7 @@ describe ErrorTracking::ListProjectsService do
end end
it 'returns error' do it 'returns error' do
expect(result).to include(status: :error, message: 'access denied') expect(result).to include(status: :error, message: 'Access denied', http_status: :unauthorized)
end end
end end
......
# frozen_string_literal: true
shared_examples 'error tracking service data not ready' do |service_call|
context "when #{service_call} returns nil" do
before do
expect(error_tracking_setting)
.to receive(service_call).and_return(nil)
end
it 'result is not ready' do
expect(result).to eq(
status: :error, http_status: :no_content, message: 'Not ready. Try again later')
end
end
end
shared_examples 'error tracking service sentry error handling' do |service_call|
context "when #{service_call} returns error" do
before do
allow(error_tracking_setting)
.to receive(service_call)
.and_return(
error: 'Sentry response status code: 401',
error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE
)
end
it 'returns the error' do
expect(result).to eq(
status: :error,
http_status: :bad_request,
message: 'Sentry response status code: 401'
)
end
end
end
shared_examples 'error tracking service http status handling' do |service_call|
context "when #{service_call} returns error with http_status" do
before do
allow(error_tracking_setting)
.to receive(service_call)
.and_return(
error: 'Sentry API response is missing keys. key not found: "id"',
error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_MISSING_KEYS
)
end
it 'returns the error with correct http_status' do
expect(result).to eq(
status: :error,
http_status: :internal_server_error,
message: 'Sentry API response is missing keys. key not found: "id"'
)
end
end
end
shared_examples 'error tracking service unauthorized user' do
context 'with unauthorized user' do
let(:unauthorized_user) { create(:user) }
subject { described_class.new(project, unauthorized_user) }
it 'returns error' do
result = subject.execute
expect(result).to include(
status: :error,
message: 'Access denied',
http_status: :unauthorized
)
end
end
end
shared_examples 'error tracking service disabled' do
context 'with error tracking disabled' do
before do
error_tracking_setting.enabled = false
end
it 'raises error' do
result = subject.execute
expect(result).to include(status: :error, message: 'Error Tracking is not enabled')
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment