Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
74780f24f2
commit
b5820a6bcd
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Track usage pings when MR gets locked/unlocked
|
||||
merge_request: 55069
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Expose container_registry_image_prefix to project API
|
||||
merge_request: 54090
|
||||
author: Mathieu Parent
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Merge Sentry's contexts into Gitlab::ApplicationContext
|
||||
merge_request: 53691
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Show nested environments when change tab, page
|
||||
merge_request: 55167
|
||||
author:
|
||||
type: changed
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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`)
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue