Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-03-03 12:11:16 +00:00
parent 74780f24f2
commit b5820a6bcd
51 changed files with 1000 additions and 319 deletions

View File

@ -446,6 +446,8 @@
.frontend:rules:bundle-size-review:
rules:
- <<: *if-not-canonical-namespace
when: never
- if: '$DANGER_GITLAB_API_TOKEN && $CI_MERGE_REQUEST_IID && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main")'
changes: *frontend-patterns
allow_failure: true

View File

@ -16,6 +16,7 @@ export default {
let params = {
scope,
page: '1',
nested: true,
};
params = this.onChangeWithFilter(params);
@ -27,6 +28,7 @@ export default {
/* URLS parameters are strings, we need to parse to match types */
let params = {
page: Number(page).toString(),
nested: true,
};
if (this.scope) {

View File

@ -29,7 +29,6 @@ class ApplicationController < ActionController::Base
before_action :validate_user_service_ticket!
before_action :check_password_expiration, if: :html_request?
before_action :ldap_security_check
around_action :sentry_context
before_action :default_headers
before_action :default_cache_headers
before_action :add_gon_variables, if: :html_request?
@ -171,7 +170,12 @@ class ApplicationController < ActionController::Base
end
def log_exception(exception)
Gitlab::ErrorTracking.track_exception(exception)
# At this point, the controller already exits set_current_context around
# block. To maintain the context while handling error exception, we need to
# set the context again
set_current_context do
Gitlab::ErrorTracking.track_exception(exception)
end
backtrace_cleaner = request.env["action_dispatch.backtrace_cleaner"]
application_trace = ActionDispatch::ExceptionWrapper.new(backtrace_cleaner, exception).application_trace
@ -528,10 +532,6 @@ class ApplicationController < ActionController::Base
.execute
end
def sentry_context(&block)
Gitlab::ErrorTracking.with_context(current_user, &block)
end
def allow_gitaly_ref_name_caching
::Gitlab::GitalyClient.allow_ref_name_caching do
yield

View File

@ -4,6 +4,8 @@ class GraphqlController < ApplicationController
# Unauthenticated users have access to the API for public data
skip_before_action :authenticate_user!
WHITELIST_HEADER = 'HTTP_X_GITLAB_QUERY_WHITELIST_ISSUE'
# If a user is using their session to access GraphQL, we need to have session
# storage, since the admin-mode check is session wide.
# We can't enable this for anonymous users because that would cause users using
@ -21,6 +23,7 @@ class GraphqlController < ApplicationController
before_action(only: [:execute]) { authenticate_sessionless_user!(:api) }
before_action :set_user_last_activity
before_action :track_vs_code_usage
before_action :whitelist_query!
# Since we deactivate authentication from the main ApplicationController and
# defer it to :authorize_access_api!, we need to override the bypass session
@ -59,6 +62,14 @@ class GraphqlController < ApplicationController
private
# Tests may mark some queries as exempt from query limits
def whitelist_query!
whitelist_issue = request.headers[WHITELIST_HEADER]
return unless whitelist_issue
Gitlab::QueryLimiting.whitelist(whitelist_issue)
end
def set_user_last_activity
return unless current_user
@ -66,7 +77,8 @@ class GraphqlController < ApplicationController
end
def track_vs_code_usage
Gitlab::UsageDataCounters::VSCodeExtensionActivityUniqueCounter.track_api_request_when_trackable(user_agent: request.user_agent, user: current_user)
Gitlab::UsageDataCounters::VSCodeExtensionActivityUniqueCounter
.track_api_request_when_trackable(user_agent: request.user_agent, user: current_user)
end
def execute_multiplex

View File

@ -1,8 +0,0 @@
# frozen_string_literal: true
# Placeholder class for model that is implemented in EE
class Iterations::Cadence < ApplicationRecord
self.table_name = 'iterations_cadences'
end
Iterations::Cadence.prepend_if_ee('::EE::Iterations::Cadence')

View File

@ -181,7 +181,7 @@ module MergeRequests
}
if exception
Gitlab::ErrorTracking.with_context(current_user) do
Gitlab::ApplicationContext.with_context(user: current_user) do
Gitlab::ErrorTracking.track_exception(exception, data)
end

View File

@ -47,6 +47,7 @@ module MergeRequests
handle_draft_status_change(merge_request, changed_fields)
track_title_and_desc_edits(changed_fields)
track_discussion_lock_toggle(merge_request, changed_fields)
notify_if_labels_added(merge_request, old_labels)
notify_if_mentions_added(merge_request, old_mentioned_users)
@ -95,6 +96,16 @@ module MergeRequests
end
end
def track_discussion_lock_toggle(merge_request, changed_fields)
return unless changed_fields.include?('discussion_locked')
if merge_request.discussion_locked
merge_request_activity_counter.track_discussion_locked_action(user: current_user)
else
merge_request_activity_counter.track_discussion_unlocked_action(user: current_user)
end
end
def notify_if_labels_added(merge_request, old_labels)
added_labels = merge_request.labels - old_labels

View File

@ -0,0 +1,5 @@
---
title: Track usage pings when MR gets locked/unlocked
merge_request: 55069
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Expose container_registry_image_prefix to project API
merge_request: 54090
author: Mathieu Parent
type: added

View File

@ -0,0 +1,5 @@
---
title: Merge Sentry's contexts into Gitlab::ApplicationContext
merge_request: 53691
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Show nested environments when change tab, page
merge_request: 55167
author:
type: changed

View File

@ -0,0 +1,8 @@
---
name: usage_data_i_code_review_user_mr_discussion_locked
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55069
rollout_issue_url:
milestone: '13.10'
type: development
group: group::code review
default_enabled: true

View File

@ -0,0 +1,8 @@
---
name: usage_data_i_code_review_user_mr_discussion_unlocked
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55069
rollout_issue_url:
milestone: '13.10'
type: development
group: group::code review
default_enabled: true

View File

@ -0,0 +1,20 @@
---
key_path: redis_hll_counters.code_review.i_code_review_user_mr_discussion_locked_monthly
description: Count of unique users per month who locked a MR
product_section: dev
product_stage: create
product_group: group::code review
product_category: code_review
value_type: number
status: implemented
milestone: "13.10"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55069
time_frame: 28d
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,20 @@
---
key_path: redis_hll_counters.code_review.i_code_review_user_mr_discussion_unlocked_monthly
description: Count of unique users per month who unlocked a MR
product_section: dev
product_stage: create
product_group: group::code review
product_category: code_review
value_type: number
status: implemented
milestone: "13.10"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55069
time_frame: 28d
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,20 @@
---
key_path: redis_hll_counters.code_review.i_code_review_user_mr_discussion_unlocked_weekly
description: Count of unique users per week who unlocked a MR
product_section: dev
product_stage: create
product_group: group::code review
product_category: code_review
value_type: number
status: implemented
milestone: "13.10"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55069
time_frame: 7d
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,20 @@
---
key_path: redis_hll_counters.code_review.i_code_review_user_mr_discussion_locked_weekly
description: Count of unique users per week who locked a MR
product_section: dev
product_stage: create
product_group: group::code review
product_category: code_review
value_type: number
status: implemented
milestone: "13.10"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55069
time_frame: 7d
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -862,7 +862,7 @@ In installations from source:
remote_directory: "pages" # The bucket name
connection:
provider: AWS # Only AWS supported at the moment
aws_access_key_id: AWS_ACESS_KEY_ID
aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
region: eu-central-1
```
@ -969,14 +969,15 @@ to define the explicit address that the GitLab Pages daemon should listen on:
gitlab_pages['listen_proxy'] = '127.0.0.1:8090'
```
### 404 error after transferring project to a different group or user
### 404 error after transferring the project to a different group or user, or changing project path
If you encounter a `404 Not Found` error a Pages site after transferring a project to
another group or user, you must trigger a domain configuration update for Pages. To do
so, write something in the `.update` file. The Pages daemon monitors for changes to this
file, and reloads the configuration when changes occur.
another group or user, or changing project path, you must trigger a domain configuration
update for Pages. To do so, write something in the `.update` file. The Pages daemon
monitors for changes to this file, and reloads the configuration when changes occur.
Use this example to fix a `404 Not Found` error after transferring a project with Pages:
Use this example to fix a `404 Not Found` error after transferring a project or changing
a project path with Pages:
```shell
date > /var/opt/gitlab/gitlab-rails/shared/pages/.update

View File

@ -24,7 +24,7 @@ be textually exported. This ensures that:
## Configuration
To configure the pseudonymizer, you need to:
To configure the Pseudonymizer, you need to:
- Provide a manifest file that describes which fields should be included or
pseudonymized ([example `manifest.yml` file](https://gitlab.com/gitlab-org/gitlab/tree/master/config/pseudonymizer.yml)).
@ -87,7 +87,7 @@ To configure the pseudonymizer, you need to:
## Usage
You can optionally run the pseudonymizer using the following environment variables:
You can optionally run the Pseudonymizer using the following environment variables:
- `PSEUDONYMIZER_OUTPUT_DIR` - where to store the output CSV files (defaults to `/tmp`)
- `PSEUDONYMIZER_BATCH` - the batch size when querying the DB (defaults to `100000`)

View File

@ -121,7 +121,7 @@ See [the available connection settings for different providers](object_storage.m
remote_directory: "terraform" # The bucket name
connection:
provider: AWS # Only AWS supported at the moment
aws_access_key_id: AWS_ACESS_KEY_ID
aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
region: eu-central-1
```

View File

@ -146,7 +146,7 @@ _The uploads are stored by default in
remote_directory: "uploads" # The bucket name
connection:
provider: AWS # Only AWS supported at the moment
aws_access_key_id: AWS_ACESS_KEY_ID
aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
region: eu-central-1
```

View File

@ -179,6 +179,7 @@ When the user is authenticated and `simple` is not set this returns something li
"packages_size": 0,
"snippets_size": 0
},
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-client",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
@ -284,6 +285,7 @@ When the user is authenticated and `simple` is not set this returns something li
"packages_size": 0,
"snippets_size": 0
},
"container_registry_image_prefix": "registry.example.com/brightbox/puppet",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
@ -439,6 +441,7 @@ GET /users/:user_id/projects
"packages_size": 0,
"snippets_size": 0
},
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-client",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
@ -544,6 +547,7 @@ GET /users/:user_id/projects
"packages_size": 0,
"snippets_size": 0
},
"container_registry_image_prefix": "registry.example.com/brightbox/puppet",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
@ -658,6 +662,7 @@ Example response:
"lfs_objects_size": 0,
"job_artifacts_size": 0
},
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-client",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
@ -758,6 +763,7 @@ Example response:
"lfs_objects_size": 0,
"job_artifacts_size": 0
},
"container_registry_image_prefix": "registry.example.com/brightbox/puppet",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
@ -921,6 +927,7 @@ GET /projects/:id
"packages_size": 0,
"snippets_size": 0
},
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-client",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
@ -1373,6 +1380,7 @@ Example responses:
"merge_method": "merge",
"autoclose_referenced_issues": true,
"suggestion_commit_message": null,
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-project-site",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
@ -1467,6 +1475,7 @@ Example response:
"merge_method": "merge",
"autoclose_referenced_issues": true,
"suggestion_commit_message": null,
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-project-site",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
@ -1559,6 +1568,7 @@ Example response:
"merge_method": "merge",
"autoclose_referenced_issues": true,
"suggestion_commit_message": null,
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-project-site",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
@ -1745,6 +1755,7 @@ Example response:
"merge_method": "merge",
"autoclose_referenced_issues": true,
"suggestion_commit_message": null,
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-project-site",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
@ -1858,6 +1869,7 @@ Example response:
"merge_method": "merge",
"autoclose_referenced_issues": true,
"suggestion_commit_message": null,
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-project-site",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
@ -2354,6 +2366,7 @@ Example response:
"avatar_url": null,
"web_url": "https://gitlab.example.com/groups/cute-cats"
},
"container_registry_image_prefix": "registry.example.com/cute-cats/hello-world",
"_links": {
"self": "https://gitlab.example.com/api/v4/projects/7",
"issues": "https://gitlab.example.com/api/v4/projects/7/issues",

View File

@ -13384,6 +13384,86 @@ Count of unique users per week|month who merged a MR
| `tier` | |
| `skip_validation` | true |
## `redis_hll_counters.code_review.i_code_review_user_mr_discussion_locked_monthly`
Count of unique users per month who locked a MR
| field | value |
| --- | --- |
| `key_path` | **`redis_hll_counters.code_review.i_code_review_user_mr_discussion_locked_monthly`** |
| `product_section` | dev |
| `product_stage` | create |
| `product_group` | `group::code review` |
| `product_category` | `code_review` |
| `value_type` | number |
| `status` | implemented |
| `milestone` | 13.10 |
| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55069) |
| `time_frame` | 28d |
| `data_source` | Redis_hll |
| `distribution` | ce, ee |
| `tier` | free, premium, ultimate |
## `redis_hll_counters.code_review.i_code_review_user_mr_discussion_locked_weekly`
Count of unique users per week who locked a MR
| field | value |
| --- | --- |
| `key_path` | **`redis_hll_counters.code_review.i_code_review_user_mr_discussion_locked_weekly`** |
| `product_section` | dev |
| `product_stage` | create |
| `product_group` | `group::code review` |
| `product_category` | `code_review` |
| `value_type` | number |
| `status` | implemented |
| `milestone` | 13.10 |
| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55069) |
| `time_frame` | 7d |
| `data_source` | Redis_hll |
| `distribution` | ce, ee |
| `tier` | free, premium, ultimate |
## `redis_hll_counters.code_review.i_code_review_user_mr_discussion_unlocked_monthly`
Count of unique users per month who unlocked a MR
| field | value |
| --- | --- |
| `key_path` | **`redis_hll_counters.code_review.i_code_review_user_mr_discussion_unlocked_monthly`** |
| `product_section` | dev |
| `product_stage` | create |
| `product_group` | `group::code review` |
| `product_category` | `code_review` |
| `value_type` | number |
| `status` | implemented |
| `milestone` | 13.10 |
| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55069) |
| `time_frame` | 28d |
| `data_source` | Redis_hll |
| `distribution` | ce, ee |
| `tier` | free, premium, ultimate |
## `redis_hll_counters.code_review.i_code_review_user_mr_discussion_unlocked_weekly`
Count of unique users per week who unlocked a MR
| field | value |
| --- | --- |
| `key_path` | **`redis_hll_counters.code_review.i_code_review_user_mr_discussion_unlocked_weekly`** |
| `product_section` | dev |
| `product_stage` | create |
| `product_group` | `group::code review` |
| `product_category` | `code_review` |
| `value_type` | number |
| `status` | implemented |
| `milestone` | 13.10 |
| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55069) |
| `time_frame` | 7d |
| `data_source` | Redis_hll |
| `distribution` | ce, ee |
| `tier` | free, premium, ultimate |
## `redis_hll_counters.code_review.i_code_review_user_publish_review_monthly`
Missing description

View File

@ -5,6 +5,8 @@ module API
class Project < BasicProjectDetails
include ::API::Helpers::RelatedResourcesHelpers
expose :container_registry_url, as: :container_registry_image_prefix, if: -> (_, _) { Gitlab.config.registry.enabled }
expose :_links do
expose :self do |project|
expose_url(api_v4_projects_path(id: project.id))

View File

@ -467,7 +467,7 @@ module API
def handle_api_exception(exception)
if report_exception?(exception)
define_params_for_grape_middleware
Gitlab::ErrorTracking.with_context(current_user) do
Gitlab::ApplicationContext.with_context(user: current_user) do
Gitlab::ErrorTracking.track_exception(exception)
end
end

View File

@ -27,8 +27,12 @@ module Gitlab
Labkit::Context.push(application_context.to_lazy_hash)
end
def self.current
Labkit::Context.current.to_h
end
def self.current_context_include?(attribute_name)
Labkit::Context.current.to_h.include?(Labkit::Context.log_key(attribute_name))
current.include?(Labkit::Context.log_key(attribute_name))
end
def initialize(**args)

View File

@ -27,33 +27,16 @@ module Gitlab
config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s)
config.processors << ::Gitlab::ErrorTracking::Processor::SidekiqProcessor
config.processors << ::Gitlab::ErrorTracking::Processor::GrpcErrorProcessor
config.processors << ::Gitlab::ErrorTracking::Processor::ContextPayloadProcessor
# Sanitize authentication headers
config.sanitize_http_headers = %w[Authorization Private-Token]
config.tags = extra_tags_from_env.merge(program: Gitlab.process_name)
config.before_send = method(:before_send)
yield config if block_given?
end
end
def with_context(current_user = nil)
last_user_context = Raven.context.user
user_context = {
id: current_user&.id,
email: current_user&.email,
username: current_user&.username
}.compact
Raven.tags_context(default_tags)
Raven.user_context(user_context)
yield
ensure
Raven.user_context(last_user_context)
end
# This should be used when you want to passthrough exception handling:
# rescue and raise to be catched in upper layers of the application.
#
@ -118,37 +101,20 @@ module Gitlab
end
def process_exception(exception, sentry: false, logging: true, extra:)
exception.try(:sentry_extra_data)&.tap do |data|
extra = extra.merge(data) if data.is_a?(Hash)
end
extra = sanitize_request_parameters(extra)
context_payload = Gitlab::ErrorTracking::ContextPayloadGenerator.generate(exception, extra)
if sentry && Raven.configuration.server
Raven.capture_exception(exception, tags: default_tags, extra: extra)
Raven.capture_exception(exception, **context_payload)
end
if logging
# TODO: this logic could migrate into `Gitlab::ExceptionLogFormatter`
# and we could also flatten deep nested hashes if required for search
# (e.g. if `extra` includes hash of hashes).
# In the current implementation, we don't flatten multi-level folded hashes.
log_hash = {}
Raven.context.tags.each { |name, value| log_hash["tags.#{name}"] = value }
Raven.context.user.each { |name, value| log_hash["user.#{name}"] = value }
Raven.context.extra.merge(extra).each { |name, value| log_hash["extra.#{name}"] = value }
Gitlab::ExceptionLogFormatter.format!(exception, log_hash)
formatter = Gitlab::ErrorTracking::LogFormatter.new
log_hash = formatter.generate_log(exception, context_payload)
Gitlab::ErrorTracking::Logger.error(log_hash)
end
end
def sanitize_request_parameters(parameters)
filter = ActiveSupport::ParameterFilter.new(::Rails.application.config.filter_parameters)
filter.filter(parameters)
end
def sentry_dsn
return unless Rails.env.production? || Rails.env.development?
return unless Gitlab.config.sentry.enabled
@ -160,22 +126,6 @@ module Gitlab
Rails.env.development? || Rails.env.test?
end
def default_tags
{
Labkit::Correlation::CorrelationId::LOG_KEY.to_sym => Labkit::Correlation::CorrelationId.current_id,
locale: I18n.locale
}
end
# Static tags that are set on application start
def extra_tags_from_env
Gitlab::Json.parse(ENV.fetch('GITLAB_SENTRY_EXTRA_TAGS', '{}')).to_hash
rescue => e
Gitlab::AppLogger.debug("GITLAB_SENTRY_EXTRA_TAGS could not be parsed as JSON: #{e.class.name}: #{e.message}")
{}
end
# Group common, mostly non-actionable exceptions by type and message,
# rather than cause
def custom_fingerprinting(event, ex)

View File

@ -0,0 +1,66 @@
# frozen_string_literal: true
module Gitlab
module ErrorTracking
class ContextPayloadGenerator
def self.generate(exception, extra = {})
new.generate(exception, extra)
end
def generate(exception, extra = {})
{
extra: extra_payload(exception, extra),
tags: tags_payload,
user: user_payload
}
end
private
def extra_payload(exception, extra)
inline_extra = exception.try(:sentry_extra_data)
if inline_extra.present? && inline_extra.is_a?(Hash)
extra = extra.merge(inline_extra)
end
sanitize_request_parameters(extra)
end
def sanitize_request_parameters(parameters)
filter = ActiveSupport::ParameterFilter.new(::Rails.application.config.filter_parameters)
filter.filter(parameters)
end
def tags_payload
extra_tags_from_env.merge!(
program: Gitlab.process_name,
locale: I18n.locale,
feature_category: current_context['meta.feature_category'],
Labkit::Correlation::CorrelationId::LOG_KEY.to_sym => Labkit::Correlation::CorrelationId.current_id
)
end
def user_payload
{
username: current_context['meta.user']
}
end
# Static tags that are set on application start
def extra_tags_from_env
Gitlab::Json.parse(ENV.fetch('GITLAB_SENTRY_EXTRA_TAGS', '{}')).to_hash
rescue => e
Gitlab::AppLogger.debug("GITLAB_SENTRY_EXTRA_TAGS could not be parsed as JSON: #{e.class.name}: #{e.message}")
{}
end
def current_context
# In case Gitlab::ErrorTracking is used when the app starts
return {} unless defined?(::Gitlab::ApplicationContext)
::Gitlab::ApplicationContext.current.to_h
end
end
end
end

View File

@ -0,0 +1,55 @@
# frozen_string_literal: true
module Gitlab
module ErrorTracking
class LogFormatter
# Note: all the accesses to Raven's contexts here are to keep the
# backward-compatibility to Sentry's built-in integrations. In future,
# they can be removed.
def generate_log(exception, context_payload)
payload = {}
Gitlab::ExceptionLogFormatter.format!(exception, payload)
append_user_to_log!(payload, context_payload)
append_tags_to_log!(payload, context_payload)
append_extra_to_log!(payload, context_payload)
payload
end
private
def append_user_to_log!(payload, context_payload)
user_context = Raven.context.user.merge(context_payload[:user])
user_context.each do |key, value|
payload["user.#{key}"] = value
end
end
def append_tags_to_log!(payload, context_payload)
tags_context = Raven.context.tags.merge(context_payload[:tags])
tags_context.each do |key, value|
payload["tags.#{key}"] = value
end
end
def append_extra_to_log!(payload, context_payload)
extra = Raven.context.extra.merge(context_payload[:extra])
extra = extra.except(:server)
# The extra value for sidekiq is a hash whose keys are strings.
if extra[:sidekiq].is_a?(Hash) && extra[:sidekiq].key?('args')
sidekiq_extra = extra.delete(:sidekiq)
sidekiq_extra['args'] = Gitlab::ErrorTracking::Processor::SidekiqProcessor.loggable_arguments(
sidekiq_extra['args'], sidekiq_extra['class']
)
payload["extra.sidekiq"] = sidekiq_extra
end
extra.each do |key, value|
payload["extra.#{key}"] = value
end
end
end
end
end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
module Gitlab
module ErrorTracking
module Processor
class ContextPayloadProcessor < ::Raven::Processor
# This processor is added to inject application context into Sentry
# events generated by Sentry built-in integrations. When the
# integrations are re-implemented and use Gitlab::ErrorTracking, this
# processor should be removed.
def process(payload)
context_payload = Gitlab::ErrorTracking::ContextPayloadGenerator.generate(nil, {})
payload.deep_merge!(context_payload)
end
end
end
end
end

View File

@ -12,16 +12,6 @@ module Gitlab
'exception.message' => exception.message
)
payload.delete('extra.server')
payload['extra.sidekiq'].tap do |value|
if value.is_a?(Hash) && value.key?('args')
value = value.dup
payload['extra.sidekiq']['args'] = Gitlab::ErrorTracking::Processor::SidekiqProcessor
.loggable_arguments(value['args'], value['class'])
end
end
if exception.backtrace
payload['exception.backtrace'] = Rails.backtrace_cleaner.clean(exception.backtrace)
end

View File

@ -42,7 +42,9 @@
'i_code_review_user_approval_rule_edited',
'i_code_review_user_vs_code_api_request',
'i_code_review_user_toggled_task_item_status',
'i_code_review_user_create_mr_from_issue'
'i_code_review_user_create_mr_from_issue',
'i_code_review_user_mr_discussion_locked',
'i_code_review_user_mr_discussion_unlocked'
]
- name: code_review_category_monthly_active_users
operator: OR
@ -78,7 +80,9 @@
'i_code_review_user_approval_rule_deleted',
'i_code_review_user_approval_rule_edited',
'i_code_review_user_toggled_task_item_status',
'i_code_review_user_create_mr_from_issue'
'i_code_review_user_create_mr_from_issue',
'i_code_review_user_mr_discussion_locked',
'i_code_review_user_mr_discussion_unlocked'
]
- name: code_review_extension_category_monthly_active_users
operator: OR

View File

@ -164,3 +164,13 @@
category: code_review
aggregation: weekly
feature_flag: usage_data_i_code_review_user_create_mr_from_issue
- name: i_code_review_user_mr_discussion_locked
redis_slot: code_review
category: code_review
aggregation: weekly
feature_flag: usage_data_i_code_review_user_mr_discussion_locked
- name: i_code_review_user_mr_discussion_unlocked
redis_slot: code_review
category: code_review
aggregation: weekly
feature_flag: usage_data_i_code_review_user_mr_discussion_unlocked

View File

@ -35,6 +35,8 @@ module Gitlab
MR_EDIT_MR_TITLE_ACTION = 'i_code_review_edit_mr_title'
MR_EDIT_MR_DESC_ACTION = 'i_code_review_edit_mr_desc'
MR_CREATE_FROM_ISSUE_ACTION = 'i_code_review_user_create_mr_from_issue'
MR_DISCUSSION_LOCKED_ACTION = 'i_code_review_user_mr_discussion_locked'
MR_DISCUSSION_UNLOCKED_ACTION = 'i_code_review_user_mr_discussion_unlocked'
class << self
def track_mr_diffs_action(merge_request:)
@ -153,6 +155,14 @@ module Gitlab
track_unique_action_by_user(MR_CREATE_FROM_ISSUE_ACTION, user)
end
def track_discussion_locked_action(user:)
track_unique_action_by_user(MR_DISCUSSION_LOCKED_ACTION, user)
end
def track_discussion_unlocked_action(user:)
track_unique_action_by_user(MR_DISCUSSION_UNLOCKED_ACTION, user)
end
private
def track_unique_action_by_merge_request(action, merge_request)

View File

@ -21028,6 +21028,9 @@ msgstr ""
msgid "OnCallSchedules|Restrict to time intervals"
msgstr ""
msgid "OnCallSchedules|Rotation end date/time must come after start date/time"
msgstr ""
msgid "OnCallSchedules|Rotation length"
msgstr ""

View File

@ -58,7 +58,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "1.184.0",
"@gitlab/tributejs": "1.0.0",
"@gitlab/ui": "28.0.0",
"@gitlab/ui": "28.1.0",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-4",
"@rails/ujs": "^6.0.3-4",

View File

@ -314,7 +314,7 @@ function deploy() {
if [[ "$(base_config_changed)" == "true" ]]; then base_config_file_ref="${CI_COMMIT_SHA}"; fi
local base_config_file="https://gitlab.com/gitlab-org/gitlab/raw/${base_config_file_ref}/scripts/review_apps/base-config.yaml"
echoinfo "Deploying ${release}..." true
echoinfo "Deploying ${release} to ${CI_ENVIRONMENT_URL} ..." true
IMAGE_REPOSITORY="registry.gitlab.com/gitlab-org/build/cng-mirror"
gitlab_migrations_image_repository="${IMAGE_REPOSITORY}/gitlab-rails-ee"

View File

@ -97,13 +97,21 @@ describe('Environment', () => {
jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
wrapper.find('.gl-pagination li:nth-child(3) .page-link').trigger('click');
expect(wrapper.vm.updateContent).toHaveBeenCalledWith({ scope: 'available', page: '2' });
expect(wrapper.vm.updateContent).toHaveBeenCalledWith({
scope: 'available',
page: '2',
nested: true,
});
});
it('should make an API request when using tabs', () => {
jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
findEnvironmentsTabStopped().trigger('click');
expect(wrapper.vm.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
expect(wrapper.vm.updateContent).toHaveBeenCalledWith({
scope: 'stopped',
page: '1',
nested: true,
});
});
it('should not make the same API request when clicking on the current scope tab', () => {

View File

@ -103,13 +103,18 @@ describe('Environments Folder View', () => {
expect(wrapper.vm.updateContent).toHaveBeenCalledWith({
scope: wrapper.vm.scope,
page: '10',
nested: true,
});
});
it('should make an API request when using tabs', () => {
jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
findEnvironmentsTabStopped().trigger('click');
expect(wrapper.vm.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
expect(wrapper.vm.updateContent).toHaveBeenCalledWith({
scope: 'stopped',
page: '1',
nested: true,
});
});
});
});
@ -161,7 +166,11 @@ describe('Environments Folder View', () => {
it('should set page to 1', () => {
jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
wrapper.vm.onChangeTab('stopped');
expect(wrapper.vm.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
expect(wrapper.vm.updateContent).toHaveBeenCalledWith({
scope: 'stopped',
page: '1',
nested: true,
});
});
});
@ -172,6 +181,7 @@ describe('Environments Folder View', () => {
expect(wrapper.vm.updateContent).toHaveBeenCalledWith({
scope: wrapper.vm.scope,
page: '4',
nested: true,
});
});
});

View File

@ -0,0 +1,176 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'rspec-parameterized'
RSpec.describe Gitlab::ErrorTracking::ContextPayloadGenerator do
subject(:generator) { described_class.new }
let(:extra) do
{
some_other_info: 'info',
issue_url: 'http://gitlab.com/gitlab-org/gitlab-foss/-/issues/1'
}
end
let(:exception) { StandardError.new("Dummy exception") }
before do
allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('cid')
allow(I18n).to receive(:locale).and_return('en')
end
context 'user metadata' do
let(:user) { create(:user) }
it 'appends user metadata to the payload' do
payload = {}
Gitlab::ApplicationContext.with_context(user: user) do
payload = generator.generate(exception, extra)
end
expect(payload[:user]).to eql(
username: user.username
)
end
end
context 'tags metadata' do
context 'when the GITLAB_SENTRY_EXTRA_TAGS env is not set' do
before do
stub_env('GITLAB_SENTRY_EXTRA_TAGS', nil)
end
it 'does not log into AppLogger' do
expect(Gitlab::AppLogger).not_to receive(:debug)
generator.generate(exception, extra)
end
it 'does not send any extra tags' do
payload = {}
Gitlab::ApplicationContext.with_context(feature_category: 'feature_a') do
payload = generator.generate(exception, extra)
end
expect(payload[:tags]).to eql(
correlation_id: 'cid',
locale: 'en',
program: 'test',
feature_category: 'feature_a'
)
end
end
context 'when the GITLAB_SENTRY_EXTRA_TAGS env is a JSON hash' do
it 'includes those tags in all events' do
stub_env('GITLAB_SENTRY_EXTRA_TAGS', { foo: 'bar', baz: 'quux' }.to_json)
payload = {}
Gitlab::ApplicationContext.with_context(feature_category: 'feature_a') do
payload = generator.generate(exception, extra)
end
expect(payload[:tags]).to eql(
correlation_id: 'cid',
locale: 'en',
program: 'test',
feature_category: 'feature_a',
'foo' => 'bar',
'baz' => 'quux'
)
end
it 'does not log into AppLogger' do
expect(Gitlab::AppLogger).not_to receive(:debug)
generator.generate(exception, extra)
end
end
context 'when the GITLAB_SENTRY_EXTRA_TAGS env is not a JSON hash' do
using RSpec::Parameterized::TableSyntax
where(:env_var, :error) do
{ foo: 'bar', baz: 'quux' }.inspect | 'JSON::ParserError'
[].to_json | 'NoMethodError'
[%w[foo bar]].to_json | 'NoMethodError'
%w[foo bar].to_json | 'NoMethodError'
'"string"' | 'NoMethodError'
end
with_them do
before do
stub_env('GITLAB_SENTRY_EXTRA_TAGS', env_var)
end
it 'logs into AppLogger' do
expect(Gitlab::AppLogger).to receive(:debug).with(a_string_matching(error))
generator.generate({})
end
it 'does not include any extra tags' do
payload = {}
Gitlab::ApplicationContext.with_context(feature_category: 'feature_a') do
payload = generator.generate(exception, extra)
end
expect(payload[:tags]).to eql(
correlation_id: 'cid',
locale: 'en',
program: 'test',
feature_category: 'feature_a'
)
end
end
end
end
context 'extra metadata' do
it 'appends extra metadata to the payload' do
payload = generator.generate(exception, extra)
expect(payload[:extra]).to eql(
some_other_info: 'info',
issue_url: 'http://gitlab.com/gitlab-org/gitlab-foss/-/issues/1'
)
end
it 'appends exception embedded extra metadata to the payload' do
allow(exception).to receive(:sentry_extra_data).and_return(
some_other_info: 'another_info',
mr_url: 'https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/1'
)
payload = generator.generate(exception, extra)
expect(payload[:extra]).to eql(
some_other_info: 'another_info',
issue_url: 'http://gitlab.com/gitlab-org/gitlab-foss/-/issues/1',
mr_url: 'https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/1'
)
end
it 'filters sensitive extra info' do
extra[:my_token] = '456'
allow(exception).to receive(:sentry_extra_data).and_return(
mr_url: 'https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/1',
another_token: '1234'
)
payload = generator.generate(exception, extra)
expect(payload[:extra]).to eql(
some_other_info: 'info',
issue_url: 'http://gitlab.com/gitlab-org/gitlab-foss/-/issues/1',
mr_url: 'https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/1',
my_token: '[FILTERED]',
another_token: '[FILTERED]'
)
end
end
end

View File

@ -0,0 +1,71 @@
# frozen_string_literal: true
require 'fast_spec_helper'
RSpec.describe Gitlab::ErrorTracking::LogFormatter do
let(:exception) { StandardError.new('boom') }
let(:context_payload) do
{
server: 'local-hostname-of-the-server',
user: {
ip_address: '127.0.0.1',
username: 'root'
},
tags: {
locale: 'en',
feature_category: 'category_a'
},
extra: {
some_other_info: 'other_info',
sidekiq: {
'class' => 'HelloWorker',
'args' => ['senstive string', 1, 2],
'another_field' => 'field'
}
}
}
end
before do
Raven.context.user[:user_flag] = 'flag'
Raven.context.tags[:shard] = 'catchall'
Raven.context.extra[:some_info] = 'info'
allow(exception).to receive(:backtrace).and_return(
[
'lib/gitlab/file_a.rb:1',
'lib/gitlab/file_b.rb:2'
]
)
end
after do
::Raven::Context.clear!
end
it 'appends error-related log fields and filters sensitive Sidekiq arguments' do
payload = described_class.new.generate_log(exception, context_payload)
expect(payload).to eql(
'exception.class' => 'StandardError',
'exception.message' => 'boom',
'exception.backtrace' => [
'lib/gitlab/file_a.rb:1',
'lib/gitlab/file_b.rb:2'
],
'user.ip_address' => '127.0.0.1',
'user.username' => 'root',
'user.user_flag' => 'flag',
'tags.locale' => 'en',
'tags.feature_category' => 'category_a',
'tags.shard' => 'catchall',
'extra.some_other_info' => 'other_info',
'extra.some_info' => 'info',
"extra.sidekiq" => {
"another_field" => "field",
"args" => ["[FILTERED]", "1", "2"],
"class" => "HelloWorker"
}
)
end
end

View File

@ -0,0 +1,45 @@
# frozen_string_literal: true
require 'fast_spec_helper'
RSpec.describe Gitlab::ErrorTracking::Processor::ContextPayloadProcessor do
subject(:processor) { described_class.new }
before do
allow_next_instance_of(Gitlab::ErrorTracking::ContextPayloadGenerator) do |generator|
allow(generator).to receive(:generate).and_return(
user: { username: 'root' },
tags: { locale: 'en', program: 'test', feature_category: 'feature_a', correlation_id: 'cid' },
extra: { some_info: 'info' }
)
end
end
it 'merges the context payload into event payload' do
payload = {
user: { ip_address: '127.0.0.1' },
tags: { priority: 'high' },
extra: { sidekiq: { class: 'SomeWorker', args: ['[FILTERED]', 1, 2] } }
}
processor.process(payload)
expect(payload).to eql(
user: {
ip_address: '127.0.0.1',
username: 'root'
},
tags: {
priority: 'high',
locale: 'en',
program: 'test',
feature_category: 'feature_a',
correlation_id: 'cid'
},
extra: {
some_info: 'info',
sidekiq: { class: 'SomeWorker', args: ['[FILTERED]', 1, 2] }
}
)
end
end

View File

@ -8,116 +8,55 @@ RSpec.describe Gitlab::ErrorTracking do
let(:exception) { RuntimeError.new('boom') }
let(:issue_url) { 'http://gitlab.com/gitlab-org/gitlab-foss/issues/1' }
let(:expected_payload_includes) do
[
{ 'exception.class' => 'RuntimeError' },
{ 'exception.message' => 'boom' },
{ 'tags.correlation_id' => 'cid' },
{ 'extra.some_other_info' => 'info' },
{ 'extra.issue_url' => 'http://gitlab.com/gitlab-org/gitlab-foss/issues/1' }
]
let(:user) { create(:user) }
let(:sentry_payload) do
{
tags: {
program: 'test',
locale: 'en',
feature_category: 'feature_a',
correlation_id: 'cid'
},
user: {
username: user.username
},
extra: {
some_other_info: 'info',
issue_url: 'http://gitlab.com/gitlab-org/gitlab-foss/issues/1'
}
}
end
let(:sentry_event) { Gitlab::Json.parse(Raven.client.transport.events.last[1]) }
let(:logger_payload) do
{
'exception.class' => 'RuntimeError',
'exception.message' => 'boom',
'tags.program' => 'test',
'tags.locale' => 'en',
'tags.feature_category' => 'feature_a',
'tags.correlation_id' => 'cid',
'user.username' => user.username,
'extra.some_other_info' => 'info',
'extra.issue_url' => 'http://gitlab.com/gitlab-org/gitlab-foss/issues/1'
}
end
before do
stub_sentry_settings
allow(described_class).to receive(:sentry_dsn).and_return(Gitlab.config.sentry.dsn)
allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('cid')
allow(I18n).to receive(:locale).and_return('en')
described_class.configure do |config|
config.encoding = 'json'
end
end
describe '.configure' do
context 'default tags from GITLAB_SENTRY_EXTRA_TAGS' do
context 'when the value is a JSON hash' do
it 'includes those tags in all events' do
stub_env('GITLAB_SENTRY_EXTRA_TAGS', { foo: 'bar', baz: 'quux' }.to_json)
described_class.configure do |config|
config.encoding = 'json'
end
described_class.track_exception(StandardError.new)
expect(sentry_event['tags'].except('correlation_id', 'locale', 'program'))
.to eq('foo' => 'bar', 'baz' => 'quux')
end
end
context 'when the value is not set' do
before do
stub_env('GITLAB_SENTRY_EXTRA_TAGS', nil)
end
it 'does not log an error' do
expect(Gitlab::AppLogger).not_to receive(:debug)
described_class.configure do |config|
config.encoding = 'json'
end
end
it 'does not send any extra tags' do
described_class.configure do |config|
config.encoding = 'json'
end
described_class.track_exception(StandardError.new)
expect(sentry_event['tags'].keys).to contain_exactly('correlation_id', 'locale', 'program')
end
end
context 'when the value is not a JSON hash' do
using RSpec::Parameterized::TableSyntax
where(:env_var, :error) do
{ foo: 'bar', baz: 'quux' }.inspect | 'JSON::ParserError'
[].to_json | 'NoMethodError'
[%w[foo bar]].to_json | 'NoMethodError'
%w[foo bar].to_json | 'NoMethodError'
'"string"' | 'NoMethodError'
end
with_them do
before do
stub_env('GITLAB_SENTRY_EXTRA_TAGS', env_var)
end
it 'does not include any extra tags' do
described_class.configure do |config|
config.encoding = 'json'
end
described_class.track_exception(StandardError.new)
expect(sentry_event['tags'].except('correlation_id', 'locale', 'program'))
.to be_empty
end
it 'logs the error class' do
expect(Gitlab::AppLogger).to receive(:debug).with(a_string_matching(error))
described_class.configure do |config|
config.encoding = 'json'
end
end
end
end
end
end
describe '.with_context' do
it 'adds the expected tags' do
described_class.with_context {}
expect(Raven.tags_context[:locale].to_s).to eq(I18n.locale.to_s)
expect(Raven.tags_context[Labkit::Correlation::CorrelationId::LOG_KEY.to_sym].to_s)
.to eq('cid')
around do |example|
Gitlab::ApplicationContext.with_context(user: user, feature_category: 'feature_a') do
example.run
end
end
@ -128,10 +67,15 @@ RSpec.describe Gitlab::ErrorTracking do
end
it 'raises the exception' do
expect(Raven).to receive(:capture_exception)
expect(Raven).to receive(:capture_exception).with(exception, sentry_payload)
expect { described_class.track_and_raise_for_dev_exception(exception) }
.to raise_error(RuntimeError)
expect do
described_class.track_and_raise_for_dev_exception(
exception,
issue_url: issue_url,
some_other_info: 'info'
)
end.to raise_error(RuntimeError, /boom/)
end
end
@ -141,19 +85,7 @@ RSpec.describe Gitlab::ErrorTracking do
end
it 'logs the exception with all attributes passed' do
expected_extras = {
some_other_info: 'info',
issue_url: 'http://gitlab.com/gitlab-org/gitlab-foss/issues/1'
}
expected_tags = {
correlation_id: 'cid'
}
expect(Raven).to receive(:capture_exception)
.with(exception,
tags: a_hash_including(expected_tags),
extra: a_hash_including(expected_extras))
expect(Raven).to receive(:capture_exception).with(exception, sentry_payload)
described_class.track_and_raise_for_dev_exception(
exception,
@ -163,8 +95,7 @@ RSpec.describe Gitlab::ErrorTracking do
end
it 'calls Gitlab::ErrorTracking::Logger.error with formatted payload' do
expect(Gitlab::ErrorTracking::Logger).to receive(:error)
.with(a_hash_including(*expected_payload_includes))
expect(Gitlab::ErrorTracking::Logger).to receive(:error).with(logger_payload)
described_class.track_and_raise_for_dev_exception(
exception,
@ -177,15 +108,19 @@ RSpec.describe Gitlab::ErrorTracking do
describe '.track_and_raise_exception' do
it 'always raises the exception' do
expect(Raven).to receive(:capture_exception)
expect(Raven).to receive(:capture_exception).with(exception, sentry_payload)
expect { described_class.track_and_raise_exception(exception) }
.to raise_error(RuntimeError)
expect do
described_class.track_and_raise_for_dev_exception(
exception,
issue_url: issue_url,
some_other_info: 'info'
)
end.to raise_error(RuntimeError, /boom/)
end
it 'calls Gitlab::ErrorTracking::Logger.error with formatted payload' do
expect(Gitlab::ErrorTracking::Logger).to receive(:error)
.with(a_hash_including(*expected_payload_includes))
expect(Gitlab::ErrorTracking::Logger).to receive(:error).with(logger_payload)
expect do
described_class.track_and_raise_exception(
@ -210,17 +145,16 @@ RSpec.describe Gitlab::ErrorTracking do
it 'calls Raven.capture_exception' do
track_exception
expect(Raven).to have_received(:capture_exception)
.with(exception,
tags: a_hash_including(correlation_id: 'cid'),
extra: a_hash_including(some_other_info: 'info', issue_url: issue_url))
expect(Raven).to have_received(:capture_exception).with(
exception,
sentry_payload
)
end
it 'calls Gitlab::ErrorTracking::Logger.error with formatted payload' do
track_exception
expect(Gitlab::ErrorTracking::Logger).to have_received(:error)
.with(a_hash_including(*expected_payload_includes))
expect(Gitlab::ErrorTracking::Logger).to have_received(:error).with(logger_payload)
end
context 'with filterable parameters' do
@ -229,8 +163,9 @@ RSpec.describe Gitlab::ErrorTracking do
it 'filters parameters' do
track_exception
expect(Gitlab::ErrorTracking::Logger).to have_received(:error)
.with(hash_including({ 'extra.test' => 1, 'extra.my_token' => '[FILTERED]' }))
expect(Gitlab::ErrorTracking::Logger).to have_received(:error).with(
hash_including({ 'extra.test' => 1, 'extra.my_token' => '[FILTERED]' })
)
end
end
@ -241,8 +176,9 @@ RSpec.describe Gitlab::ErrorTracking do
it 'includes the extra data from the exception in the tracking information' do
track_exception
expect(Raven).to have_received(:capture_exception)
.with(exception, a_hash_including(extra: a_hash_including(extra_info)))
expect(Raven).to have_received(:capture_exception).with(
exception, a_hash_including(extra: a_hash_including(extra_info))
)
end
end
@ -253,8 +189,9 @@ RSpec.describe Gitlab::ErrorTracking do
it 'just includes the other extra info' do
track_exception
expect(Raven).to have_received(:capture_exception)
.with(exception, a_hash_including(extra: a_hash_including(extra)))
expect(Raven).to have_received(:capture_exception).with(
exception, a_hash_including(extra: a_hash_including(extra))
)
end
end
@ -266,7 +203,13 @@ RSpec.describe Gitlab::ErrorTracking do
track_exception
expect(Gitlab::ErrorTracking::Logger).to have_received(:error).with(
hash_including({ 'extra.sidekiq' => { 'class' => 'PostReceive', 'args' => ['1', '{"id"=>2, "name"=>"hello"}', 'some-value', 'another-value'] } }))
hash_including(
'extra.sidekiq' => {
'class' => 'PostReceive',
'args' => ['1', '{"id"=>2, "name"=>"hello"}', 'some-value', 'another-value']
}
)
)
end
end
@ -276,9 +219,17 @@ RSpec.describe Gitlab::ErrorTracking do
it 'filters sensitive arguments before sending' do
track_exception
sentry_event = Gitlab::Json.parse(Raven.client.transport.events.last[1])
expect(sentry_event.dig('extra', 'sidekiq', 'args')).to eq(['[FILTERED]', 1, 2])
expect(Gitlab::ErrorTracking::Logger).to have_received(:error).with(
hash_including('extra.sidekiq' => { 'class' => 'UnknownWorker', 'args' => ['[FILTERED]', '1', '2'] }))
hash_including(
'extra.sidekiq' => {
'class' => 'UnknownWorker',
'args' => ['[FILTERED]', '1', '2']
}
)
)
end
end
end

View File

@ -284,4 +284,20 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl
let(:action) { described_class::MR_CREATE_FROM_ISSUE_ACTION }
end
end
describe '.track_discussion_locked_action' do
subject { described_class.track_discussion_locked_action(user: user) }
it_behaves_like 'a tracked merge request unique event' do
let(:action) { described_class::MR_DISCUSSION_LOCKED_ACTION }
end
end
describe '.track_discussion_unlocked_action' do
subject { described_class.track_discussion_unlocked_action(user: user) }
it_behaves_like 'a tracked merge request unique event' do
let(:action) { described_class::MR_DISCUSSION_UNLOCKED_ACTION }
end
end
end

View File

@ -221,7 +221,7 @@ RSpec.describe Upload do
it 'does not send a message to Sentry' do
upload = described_class.new(path: "#{__FILE__}-nope", store: ObjectStorage::Store::LOCAL)
expect(Raven).not_to receive(:capture_message)
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
upload.exist?
end

View File

@ -7,13 +7,27 @@ RSpec.describe 'getting merge request listings nested in a project' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:current_user) { create(:user) }
let_it_be(:label) { create(:label, project: project) }
let_it_be(:merge_request_a) { create(:labeled_merge_request, :unique_branches, source_project: project, labels: [label]) }
let_it_be(:merge_request_b) { create(:merge_request, :closed, :unique_branches, source_project: project) }
let_it_be(:merge_request_c) { create(:labeled_merge_request, :closed, :unique_branches, source_project: project, labels: [label]) }
let_it_be(:merge_request_d) { create(:merge_request, :locked, :unique_branches, source_project: project) }
let_it_be(:merge_request_e) { create(:merge_request, :unique_branches, source_project: project) }
let_it_be(:merge_request_a) do
create(:labeled_merge_request, :unique_branches, source_project: project, labels: [label])
end
let_it_be(:merge_request_b) do
create(:merge_request, :closed, :unique_branches, source_project: project)
end
let_it_be(:merge_request_c) do
create(:labeled_merge_request, :closed, :unique_branches, source_project: project, labels: [label])
end
let_it_be(:merge_request_d) do
create(:merge_request, :locked, :unique_branches, source_project: project)
end
let_it_be(:merge_request_e) do
create(:merge_request, :unique_branches, source_project: project)
end
let(:results) { graphql_data.dig('project', 'mergeRequests', 'nodes') }
@ -27,32 +41,38 @@ RSpec.describe 'getting merge request listings nested in a project' do
)
end
let(:query) do
query_merge_requests(all_graphql_fields_for('MergeRequest', max_depth: 1))
end
it_behaves_like 'a working graphql query' do
let(:query) do
query_merge_requests(all_graphql_fields_for('MergeRequest', max_depth: 2))
end
before do
post_graphql(query, current_user: current_user)
# We cannot call the whitelist here, since the transaction does not
# begin until we enter the controller.
headers = {
'X-GITLAB-QUERY-WHITELIST-ISSUE' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/322979'
}
post_graphql(query, current_user: current_user, headers: headers)
end
end
# The following tests are needed to guarantee that we have correctly annotated
# all the gitaly calls. Selecting combinations of fields may mask this due to
# memoization.
context 'requesting a single field' do
context 'when requesting a single field' do
let_it_be(:fresh_mr) { create(:merge_request, :unique_branches, source_project: project) }
let(:search_params) { { iids: [fresh_mr.iid.to_s] } }
let(:graphql_data) do
GitlabSchema.execute(query, context: { current_user: current_user }).to_h['data']
end
before do
project.repository.expire_branches_cache
end
let(:graphql_data) do
GitlabSchema.execute(query, context: { current_user: current_user }).to_h['data']
end
context 'selecting any single scalar field' do
context 'when selecting any single scalar field' do
where(:field) do
scalar_fields_of('MergeRequest').map { |name| [name] }
end
@ -68,7 +88,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
end
end
context 'selecting any single nested field' do
context 'when selecting any single nested field' do
where(:field, :subfield, :is_connection) do
nested_fields_of('MergeRequest').flat_map do |name, field|
type = field_type(field)
@ -95,7 +115,11 @@ RSpec.describe 'getting merge request listings nested in a project' do
end
end
shared_examples 'searching with parameters' do
shared_examples 'when searching with parameters' do
let(:query) do
query_merge_requests('iid title')
end
let(:expected) do
mrs.map { |mr| a_hash_including('iid' => mr.iid.to_s, 'title' => mr.title) }
end
@ -107,60 +131,60 @@ RSpec.describe 'getting merge request listings nested in a project' do
end
end
context 'there are no search params' do
context 'when there are no search params' do
let(:search_params) { nil }
let(:mrs) { [merge_request_a, merge_request_b, merge_request_c, merge_request_d, merge_request_e] }
it_behaves_like 'searching with parameters'
it_behaves_like 'when searching with parameters'
end
context 'the search params do not match anything' do
let(:search_params) { { iids: %w(foo bar baz) } }
context 'when the search params do not match anything' do
let(:search_params) { { iids: %w[foo bar baz] } }
let(:mrs) { [] }
it_behaves_like 'searching with parameters'
it_behaves_like 'when searching with parameters'
end
context 'searching by iids' do
context 'when searching by iids' do
let(:search_params) { { iids: mrs.map(&:iid).map(&:to_s) } }
let(:mrs) { [merge_request_a, merge_request_c] }
it_behaves_like 'searching with parameters'
it_behaves_like 'when searching with parameters'
end
context 'searching by state' do
context 'when searching by state' do
let(:search_params) { { state: :closed } }
let(:mrs) { [merge_request_b, merge_request_c] }
it_behaves_like 'searching with parameters'
it_behaves_like 'when searching with parameters'
end
context 'searching by source_branch' do
context 'when searching by source_branch' do
let(:search_params) { { source_branches: mrs.map(&:source_branch) } }
let(:mrs) { [merge_request_b, merge_request_c] }
it_behaves_like 'searching with parameters'
it_behaves_like 'when searching with parameters'
end
context 'searching by target_branch' do
context 'when searching by target_branch' do
let(:search_params) { { target_branches: mrs.map(&:target_branch) } }
let(:mrs) { [merge_request_a, merge_request_d] }
it_behaves_like 'searching with parameters'
it_behaves_like 'when searching with parameters'
end
context 'searching by label' do
context 'when searching by label' do
let(:search_params) { { labels: [label.title] } }
let(:mrs) { [merge_request_a, merge_request_c] }
it_behaves_like 'searching with parameters'
it_behaves_like 'when searching with parameters'
end
context 'searching by combination' do
context 'when searching by combination' do
let(:search_params) { { state: :closed, labels: [label.title] } }
let(:mrs) { [merge_request_c] }
it_behaves_like 'searching with parameters'
it_behaves_like 'when searching with parameters'
end
context 'when requesting `approved_by`' do
@ -203,10 +227,10 @@ RSpec.describe 'getting merge request listings nested in a project' do
it 'exposes `commit_count`' do
execute_query
expect(results).to match_array([
expect(results).to match_array [
{ "iid" => merge_request_a.iid.to_s, "commitCount" => 0 },
{ "iid" => merge_request_with_commits.iid.to_s, "commitCount" => 29 }
])
]
end
end
@ -216,8 +240,8 @@ RSpec.describe 'getting merge request listings nested in a project' do
before do
# make the MRs "merged"
[merge_request_a, merge_request_b, merge_request_c].each do |mr|
mr.update_column(:state_id, MergeRequest.available_states[:merged])
mr.metrics.update_column(:merged_at, Time.now)
mr.update!(state_id: MergeRequest.available_states[:merged])
mr.metrics.update!(merged_at: Time.now)
end
end
@ -256,13 +280,12 @@ RSpec.describe 'getting merge request listings nested in a project' do
end
it 'returns the reviewers' do
nodes = merge_request_a.reviewers.map { |r| { 'username' => r.username } }
reviewers = { 'nodes' => match_array(nodes) }
execute_query
expect(results).to include a_hash_including('reviewers' => {
'nodes' => match_array(merge_request_a.reviewers.map do |r|
a_hash_including('username' => r.username)
end)
})
expect(results).to include a_hash_including('reviewers' => match(reviewers))
end
include_examples 'N+1 query check'
@ -309,12 +332,14 @@ RSpec.describe 'getting merge request listings nested in a project' do
allow(Gitlab::Database).to receive(:read_only?).and_return(false)
end
def query_context
{ current_user: current_user }
end
def run_query(number)
# Ensure that we have a fresh request store and batch-context between runs
result = run_with_clean_state(query,
context: { current_user: current_user },
variables: { first: number }
)
vars = { first: number }
result = run_with_clean_state(query, context: query_context, variables: vars)
graphql_dig_at(result.to_h, :data, :project, :merge_requests, :nodes)
end
@ -348,13 +373,11 @@ RSpec.describe 'getting merge request listings nested in a project' do
let(:data_path) { [:project, :mergeRequests] }
def pagination_query(params)
graphql_query_for(:project, { full_path: project.full_path },
<<~QUERY
graphql_query_for(:project, { full_path: project.full_path }, <<~QUERY)
mergeRequests(#{params}) {
#{page_info} nodes { id }
}
QUERY
)
QUERY
end
context 'when sorting by merged_at DESC' do
@ -396,14 +419,12 @@ RSpec.describe 'getting merge request listings nested in a project' do
let(:query) do
# Note: __typename meta field is always requested by the FE
graphql_query_for(:project, { full_path: project.full_path },
<<~QUERY
graphql_query_for(:project, { full_path: project.full_path }, <<~QUERY)
mergeRequests(mergedAfter: "2020-01-01", mergedBefore: "2020-01-05", first: 0, sourceBranches: null, labels: null) {
count
__typename
}
QUERY
)
end
shared_examples 'count examples' do
@ -430,14 +451,12 @@ RSpec.describe 'getting merge request listings nested in a project' do
context 'when total_time_to_merge and count is queried' do
let(:query) do
graphql_query_for(:project, { full_path: project.full_path },
<<~QUERY
graphql_query_for(:project, { full_path: project.full_path }, <<~QUERY)
mergeRequests(mergedAfter: "2020-01-01", mergedBefore: "2020-01-05", first: 0) {
totalTimeToMerge
count
}
QUERY
)
end
it 'does not query the merge requests table for the total_time_to_merge' do

View File

@ -314,14 +314,13 @@ RSpec.describe API::Helpers do
expect(Gitlab::ErrorTracking).to receive(:sentry_dsn).and_return(Gitlab.config.sentry.dsn)
Gitlab::ErrorTracking.configure
Raven.client.configuration.encoding = 'json'
end
it 'does not report a MethodNotAllowed exception to Sentry' do
exception = Grape::Exceptions::MethodNotAllowed.new({ 'X-GitLab-Test' => '1' })
allow(exception).to receive(:backtrace).and_return(caller)
expect(Raven).not_to receive(:capture_exception).with(exception)
expect(Gitlab::ErrorTracking).not_to receive(:track_exception).with(exception)
handle_api_exception(exception)
end
@ -330,8 +329,7 @@ RSpec.describe API::Helpers do
exception = RuntimeError.new('test error')
allow(exception).to receive(:backtrace).and_return(caller)
expect(Raven).to receive(:capture_exception).with(exception, tags:
a_hash_including(correlation_id: 'new-correlation-id'), extra: {})
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(exception)
Labkit::Correlation::CorrelationId.use_id('new-correlation-id') do
handle_api_exception(exception)
@ -357,20 +355,6 @@ RSpec.describe API::Helpers do
expect(json_response['message']).to start_with("\nRuntimeError (Runtime Error!):")
end
end
context 'extra information' do
# Sentry events are an array of the form [auth_header, data, options]
let(:event_data) { Raven.client.transport.events.first[1] }
it 'sends the params, excluding confidential values' do
expect(ProjectsFinder).to receive(:new).and_raise('Runtime Error!')
get api('/projects', user), params: { password: 'dont_send_this', other_param: 'send_this' }
expect(event_data).to include('other_param=send_this')
expect(event_data).to include('password=********')
end
end
end
describe '.authenticate_non_get!' do

View File

@ -56,6 +56,7 @@ itself: # project
- can_create_merge_request_in
- compliance_frameworks
- container_expiration_policy
- container_registry_image_prefix
- default_branch
- empty_repo
- forks_count

View File

@ -1540,6 +1540,10 @@ RSpec.describe API::Projects do
end
context 'when authenticated as an admin' do
before do
stub_container_registry_config(enabled: true, host_port: 'registry.example.org:5000')
end
let(:project_attributes_file) { 'spec/requests/api/project_attributes.yml' }
let(:project_attributes) { YAML.load_file(project_attributes_file) }
@ -1569,7 +1573,7 @@ RSpec.describe API::Projects do
keys
end
it 'returns a project by id' do
it 'returns a project by id', :aggregate_failures do
project
project_member
group = create(:group)
@ -1587,6 +1591,7 @@ RSpec.describe API::Projects do
expect(json_response['ssh_url_to_repo']).to be_present
expect(json_response['http_url_to_repo']).to be_present
expect(json_response['web_url']).to be_present
expect(json_response['container_registry_image_prefix']).to eq("registry.example.org:5000/#{project.full_path}")
expect(json_response['owner']).to be_a Hash
expect(json_response['name']).to eq(project.name)
expect(json_response['path']).to be_present
@ -1644,9 +1649,10 @@ RSpec.describe API::Projects do
before do
project
project_member
stub_container_registry_config(enabled: true, host_port: 'registry.example.org:5000')
end
it 'returns a project by id' do
it 'returns a project by id', :aggregate_failures do
group = create(:group)
link = create(:project_group_link, project: project, group: group)
@ -1662,6 +1668,7 @@ RSpec.describe API::Projects do
expect(json_response['ssh_url_to_repo']).to be_present
expect(json_response['http_url_to_repo']).to be_present
expect(json_response['web_url']).to be_present
expect(json_response['container_registry_image_prefix']).to eq("registry.example.org:5000/#{project.full_path}")
expect(json_response['owner']).to be_a Hash
expect(json_response['name']).to eq(project.name)
expect(json_response['path']).to be_present

View File

@ -48,6 +48,8 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
end
context 'valid params' do
let(:locked) { true }
let(:opts) do
{
title: 'New title',
@ -58,7 +60,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
label_ids: [label.id],
target_branch: 'target',
force_remove_source_branch: '1',
discussion_locked: true
discussion_locked: locked
}
end
@ -117,6 +119,56 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
MergeRequests::UpdateService.new(project, user, opts).execute(draft_merge_request)
end
context 'when MR is locked' do
context 'when locked again' do
it 'does not track discussion locking' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.not_to receive(:track_discussion_locked_action)
opts[:discussion_locked] = true
MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
end
end
context 'when unlocked' do
it 'tracks dicussion unlocking' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_discussion_unlocked_action).once.with(user: user)
opts[:discussion_locked] = false
MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
end
end
end
context 'when MR is unlocked' do
let(:locked) { false }
context 'when unlocked again' do
it 'does not track discussion unlocking' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.not_to receive(:track_discussion_unlocked_action)
opts[:discussion_locked] = false
MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
end
end
context 'when locked' do
it 'tracks dicussion locking' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_discussion_locked_action).once.with(user: user)
opts[:discussion_locked] = true
MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
end
end
end
end
context 'updating milestone' do

View File

@ -907,10 +907,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
"@gitlab/ui@28.0.0":
version "28.0.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-28.0.0.tgz#712a24ba970e69eed595209194d69574851034c6"
integrity sha512-6gJKUEnqYAYxyWta/CJcSSPNu07iVrQ4m6Sxoh/Sjo7VpPE4VjLgTax3CdqET4KokUbIKGSWyH3s+IvtzDzFqA==
"@gitlab/ui@28.1.0":
version "28.1.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-28.1.0.tgz#aaec227c03c345632bcb2bc6222ed49a004b2dbd"
integrity sha512-V4AH41sRVPp4gOSNt/safVbmsF8RS7H49JuaMWtOkNqfc90ePW+OGyv5ioCYDMYLvk8BaGlmH8f01BuCgOrplA==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"