Add latest changes from gitlab-org/gitlab@master
|
|
@ -24,7 +24,7 @@ _Why do you believe this AI solution is a good way to solve this problem?_
|
|||
_What assumptions are you making about this problem and the solution?_
|
||||
|
||||
### Personas
|
||||
_What [personas](https://about.gitlab.com/handbook/product/personas/#list-of-user-personas) have this problem, who is the intended user?_
|
||||
_What [personas](https://handbook.gitlab.com/handbook/product/personas/#list-of-user-personas) have this problem, who is the intended user?_
|
||||
|
||||
## Proposal
|
||||
<!-- Explain the proposed changes, including details around usage and business drivers. -->
|
||||
|
|
|
|||
|
|
@ -95,25 +95,25 @@ If you enjoy taking notes using Post-it notes make sure you have some of them as
|
|||
|
||||
Deciding which persona we are focusing on will be part of the Day 1 discussions in the workshop. The personas we are going to consider are:
|
||||
|
||||
<!-- Choose which personas could be target users, and choose from this list during the Sprint. Personas are described at https://about.gitlab.com/handbook/product/personas/
|
||||
<!-- Choose which personas could be target users, and choose from this list during the Sprint. Personas are described at https://handbook.gitlab.com/handbook/product/personas/
|
||||
|
||||
* [Parker (Product Manager)](https://about.gitlab.com/handbook/product/personas/#parker-product-manager)
|
||||
* [Delaney (Development Team Lead)](https://about.gitlab.com/handbook/product/personas/#delaney-development-team-lead)
|
||||
* [Presley (Product Designer)](https://about.gitlab.com/handbook/product/personas/#presley-product-designer)
|
||||
* [Sasha (Software Developer)](https://about.gitlab.com/handbook/product/personas/#sasha-software-developer)
|
||||
* [Priyanka (Platform Engineer)](https://about.gitlab.com/handbook/product/personas/#priyanka-platform-engineer)
|
||||
* [Sidney (Systems Administrator)](https://about.gitlab.com/handbook/product/personas/#sidney-systems-administrator)
|
||||
* [Rachel (Release Manager)](https://about.gitlab.com/handbook/product/personas/#rachel-release-manager)
|
||||
* [Simone (Software Engineer in Test)](https://about.gitlab.com/handbook/product/personas/#simone-software-engineer-in-test)
|
||||
* [Allison (Application Ops)](https://about.gitlab.com/handbook/product/personas/#allison-application-ops)
|
||||
* [Ingrid (Infrastructure Operator)](https://about.gitlab.com/handbook/product/personas/#ingrid-infrastructure-operator)
|
||||
* [Dakota (Application Development Director)](https://about.gitlab.com/handbook/product/personas/#dakota-application-development-director)
|
||||
* [Dana (Data Analyst)](https://about.gitlab.com/handbook/product/personas/#dana-data-analyst)
|
||||
* [Eddie (Content Editor)](https://about.gitlab.com/handbook/product/personas/#eddie-content-editor)
|
||||
* [Amy (Application Security Engineer)](https://about.gitlab.com/handbook/product/personas/#amy-application-security-engineer)
|
||||
* [Isaac (Infrastructure Engineer)](https://about.gitlab.com/handbook/product/personas/#isaac-infrastructure-security-engineer)
|
||||
* [Alex (Security Operations Engineer)](https://about.gitlab.com/handbook/product/personas/#alex-security-operations-engineer)
|
||||
* [Cameron (Compliance Manager)](https://about.gitlab.com/handbook/product/personas/#cameron-compliance-manager)
|
||||
* [Parker (Product Manager)](https://handbook.gitlab.com/handbook/product/personas/#parker-product-manager)
|
||||
* [Delaney (Development Team Lead)](https://handbook.gitlab.com/handbook/product/personas/#delaney-development-team-lead)
|
||||
* [Presley (Product Designer)](https://handbook.gitlab.com/handbook/product/personas/#presley-product-designer)
|
||||
* [Sasha (Software Developer)](https://handbook.gitlab.com/handbook/product/personas/#sasha-software-developer)
|
||||
* [Priyanka (Platform Engineer)](https://handbook.gitlab.com/handbook/product/personas/#priyanka-platform-engineer)
|
||||
* [Sidney (Systems Administrator)](https://handbook.gitlab.com/handbook/product/personas/#sidney-systems-administrator)
|
||||
* [Rachel (Release Manager)](https://handbook.gitlab.com/handbook/product/personas/#rachel-release-manager)
|
||||
* [Simone (Software Engineer in Test)](https://handbook.gitlab.com/handbook/product/personas/#simone-software-engineer-in-test)
|
||||
* [Allison (Application Ops)](https://handbook.gitlab.com/handbook/product/personas/#allison-application-ops)
|
||||
* [Ingrid (Infrastructure Operator)](https://handbook.gitlab.com/handbook/product/personas/#ingrid-infrastructure-operator)
|
||||
* [Dakota (Application Development Director)](https://handbook.gitlab.com/handbook/product/personas/#dakota-application-development-director)
|
||||
* [Dana (Data Analyst)](https://handbook.gitlab.com/handbook/product/personas/#dana-data-analyst)
|
||||
* [Eddie (Content Editor)](https://handbook.gitlab.com/handbook/product/personas/#eddie-content-editor)
|
||||
* [Amy (Application Security Engineer)](https://handbook.gitlab.com/handbook/product/personas/#amy-application-security-engineer)
|
||||
* [Isaac (Infrastructure Engineer)](https://handbook.gitlab.com/handbook/product/personas/#isaac-infrastructure-security-engineer)
|
||||
* [Alex (Security Operations Engineer)](https://handbook.gitlab.com/handbook/product/personas/#alex-security-operations-engineer)
|
||||
* [Cameron (Compliance Manager)](https://handbook.gitlab.com/handbook/product/personas/#cameron-compliance-manager)
|
||||
|
||||
-->
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
* Any concepts, procedures, reference info we could add to make it easier to successfully use GitLab?
|
||||
* Include use cases, benefits, and/or goals for this work.
|
||||
* If adding content: What audience is it intended for? (What roles and scenarios?)
|
||||
For ideas, see personas at https://about.gitlab.com/handbook/product/personas/ or the persona labels at
|
||||
For ideas, see personas at https://handbook.gitlab.com/handbook/product/personas/ or the persona labels at
|
||||
https://gitlab.com/groups/gitlab-org/-/labels?subscribed=&search=persona%3A
|
||||
-->
|
||||
|
||||
|
|
|
|||
|
|
@ -18,25 +18,25 @@ The goal of this template is brevity for quick/smaller iterations. For a more th
|
|||
|
||||
<!-- Who will use this feature? If known, include any of the following: types of users (e.g. Developer), personas, or specific company roles (e.g. Release Manager). It's okay to write "Unknown" and fill this field in later.
|
||||
|
||||
Personas are described at https://about.gitlab.com/handbook/product/personas/
|
||||
Personas are described at https://handbook.gitlab.com/handbook/product/personas/
|
||||
|
||||
* [Parker (Product Manager)](https://about.gitlab.com/handbook/product/personas/#parker-product-manager)
|
||||
* [Delaney (Development Team Lead)](https://about.gitlab.com/handbook/product/personas/#delaney-development-team-lead)
|
||||
* [Presley (Product Designer)](https://about.gitlab.com/handbook/product/personas/#presley-product-designer)
|
||||
* [Sasha (Software Developer)](https://about.gitlab.com/handbook/product/personas/#sasha-software-developer)
|
||||
* [Priyanka (Platform Engineer)](https://about.gitlab.com/handbook/product/personas/#priyanka-platform-engineer)
|
||||
* [Sidney (Systems Administrator)](https://about.gitlab.com/handbook/product/personas/#sidney-systems-administrator)
|
||||
* [Rachel (Release Manager)](https://about.gitlab.com/handbook/product/personas/#rachel-release-manager)
|
||||
* [Simone (Software Engineer in Test)](https://about.gitlab.com/handbook/product/personas/#simone-software-engineer-in-test)
|
||||
* [Allison (Application Ops)](https://about.gitlab.com/handbook/product/personas/#allison-application-ops)
|
||||
* [Ingrid (Infrastructure Operator)](https://about.gitlab.com/handbook/product/personas/#ingrid-infrastructure-operator)
|
||||
* [Dakota (Application Development Director)](https://about.gitlab.com/handbook/product/personas/#dakota-application-development-director)
|
||||
* [Dana (Data Analyst)](https://about.gitlab.com/handbook/product/personas/#dana-data-analyst)
|
||||
* [Eddie (Content Editor)](https://about.gitlab.com/handbook/product/personas/#eddie-content-editor)
|
||||
* [Amy (Application Security Engineer)](https://about.gitlab.com/handbook/product/personas/#amy-application-security-engineer)
|
||||
* [Isaac (Infrastructure Engineer)](https://about.gitlab.com/handbook/product/personas/#isaac-infrastructure-security-engineer)
|
||||
* [Alex (Security Operations Engineer)](https://about.gitlab.com/handbook/product/personas/#alex-security-operations-engineer)
|
||||
* [Cameron (Compliance Manager)](https://about.gitlab.com/handbook/product/personas/#cameron-compliance-manager)
|
||||
* [Parker (Product Manager)](https://handbook.gitlab.com/handbook/product/personas/#parker-product-manager)
|
||||
* [Delaney (Development Team Lead)](https://handbook.gitlab.com/handbook/product/personas/#delaney-development-team-lead)
|
||||
* [Presley (Product Designer)](https://handbook.gitlab.com/handbook/product/personas/#presley-product-designer)
|
||||
* [Sasha (Software Developer)](https://handbook.gitlab.com/handbook/product/personas/#sasha-software-developer)
|
||||
* [Priyanka (Platform Engineer)](https://handbook.gitlab.com/handbook/product/personas/#priyanka-platform-engineer)
|
||||
* [Sidney (Systems Administrator)](https://handbook.gitlab.com/handbook/product/personas/#sidney-systems-administrator)
|
||||
* [Rachel (Release Manager)](https://handbook.gitlab.com/handbook/product/personas/#rachel-release-manager)
|
||||
* [Simone (Software Engineer in Test)](https://handbook.gitlab.com/handbook/product/personas/#simone-software-engineer-in-test)
|
||||
* [Allison (Application Ops)](https://handbook.gitlab.com/handbook/product/personas/#allison-application-ops)
|
||||
* [Ingrid (Infrastructure Operator)](https://handbook.gitlab.com/handbook/product/personas/#ingrid-infrastructure-operator)
|
||||
* [Dakota (Application Development Director)](https://handbook.gitlab.com/handbook/product/personas/#dakota-application-development-director)
|
||||
* [Dana (Data Analyst)](https://handbook.gitlab.com/handbook/product/personas/#dana-data-analyst)
|
||||
* [Eddie (Content Editor)](https://handbook.gitlab.com/handbook/product/personas/#eddie-content-editor)
|
||||
* [Amy (Application Security Engineer)](https://handbook.gitlab.com/handbook/product/personas/#amy-application-security-engineer)
|
||||
* [Isaac (Infrastructure Engineer)](https://handbook.gitlab.com/handbook/product/personas/#isaac-infrastructure-security-engineer)
|
||||
* [Alex (Security Operations Engineer)](https://handbook.gitlab.com/handbook/product/personas/#alex-security-operations-engineer)
|
||||
* [Cameron (Compliance Manager)](https://handbook.gitlab.com/handbook/product/personas/#cameron-compliance-manager)
|
||||
-->
|
||||
|
||||
### Feature Usage Metrics
|
||||
|
|
|
|||
|
|
@ -12,25 +12,25 @@
|
|||
|
||||
<!-- Who will use this feature? If known, include any of the following: types of users (e.g. Developer), personas, or specific company roles (e.g. Release Manager). It's okay to write "Unknown" and fill this field in later.
|
||||
|
||||
Personas are described at https://about.gitlab.com/handbook/product/personas/
|
||||
Personas are described at https://handbook.gitlab.com/handbook/product/personas/
|
||||
|
||||
* [Parker (Product Manager)](https://about.gitlab.com/handbook/product/personas/#parker-product-manager)
|
||||
* [Delaney (Development Team Lead)](https://about.gitlab.com/handbook/product/personas/#delaney-development-team-lead)
|
||||
* [Presley (Product Designer)](https://about.gitlab.com/handbook/product/personas/#presley-product-designer)
|
||||
* [Sasha (Software Developer)](https://about.gitlab.com/handbook/product/personas/#sasha-software-developer)
|
||||
* [Priyanka (Platform Engineer)](https://about.gitlab.com/handbook/product/personas/#priyanka-platform-engineer)
|
||||
* [Sidney (Systems Administrator)](https://about.gitlab.com/handbook/product/personas/#sidney-systems-administrator)
|
||||
* [Rachel (Release Manager)](https://about.gitlab.com/handbook/product/personas/#rachel-release-manager)
|
||||
* [Simone (Software Engineer in Test)](https://about.gitlab.com/handbook/product/personas/#simone-software-engineer-in-test)
|
||||
* [Allison (Application Ops)](https://about.gitlab.com/handbook/product/personas/#allison-application-ops)
|
||||
* [Ingrid (Infrastructure Operator)](https://about.gitlab.com/handbook/product/personas/#ingrid-infrastructure-operator)
|
||||
* [Dakota (Application Development Director)](https://about.gitlab.com/handbook/product/personas/#dakota-application-development-director)
|
||||
* [Dana (Data Analyst)](https://about.gitlab.com/handbook/product/personas/#dana-data-analyst)
|
||||
* [Eddie (Content Editor)](https://about.gitlab.com/handbook/product/personas/#eddie-content-editor)
|
||||
* [Amy (Application Security Engineer)](https://about.gitlab.com/handbook/product/personas/#amy-application-security-engineer)
|
||||
* [Isaac (Infrastructure Engineer)](https://about.gitlab.com/handbook/product/personas/#isaac-infrastructure-security-engineer)
|
||||
* [Alex (Security Operations Engineer)](https://about.gitlab.com/handbook/product/personas/#alex-security-operations-engineer)
|
||||
* [Cameron (Compliance Manager)](https://about.gitlab.com/handbook/product/personas/#cameron-compliance-manager)
|
||||
* [Parker (Product Manager)](https://handbook.gitlab.com/handbook/product/personas/#parker-product-manager)
|
||||
* [Delaney (Development Team Lead)](https://handbook.gitlab.com/handbook/product/personas/#delaney-development-team-lead)
|
||||
* [Presley (Product Designer)](https://handbook.gitlab.com/handbook/product/personas/#presley-product-designer)
|
||||
* [Sasha (Software Developer)](https://handbook.gitlab.com/handbook/product/personas/#sasha-software-developer)
|
||||
* [Priyanka (Platform Engineer)](https://handbook.gitlab.com/handbook/product/personas/#priyanka-platform-engineer)
|
||||
* [Sidney (Systems Administrator)](https://handbook.gitlab.com/handbook/product/personas/#sidney-systems-administrator)
|
||||
* [Rachel (Release Manager)](https://handbook.gitlab.com/handbook/product/personas/#rachel-release-manager)
|
||||
* [Simone (Software Engineer in Test)](https://handbook.gitlab.com/handbook/product/personas/#simone-software-engineer-in-test)
|
||||
* [Allison (Application Ops)](https://handbook.gitlab.com/handbook/product/personas/#allison-application-ops)
|
||||
* [Ingrid (Infrastructure Operator)](https://handbook.gitlab.com/handbook/product/personas/#ingrid-infrastructure-operator)
|
||||
* [Dakota (Application Development Director)](https://handbook.gitlab.com/handbook/product/personas/#dakota-application-development-director)
|
||||
* [Dana (Data Analyst)](https://handbook.gitlab.com/handbook/product/personas/#dana-data-analyst)
|
||||
* [Eddie (Content Editor)](https://handbook.gitlab.com/handbook/product/personas/#eddie-content-editor)
|
||||
* [Amy (Application Security Engineer)](https://handbook.gitlab.com/handbook/product/personas/#amy-application-security-engineer)
|
||||
* [Isaac (Infrastructure Engineer)](https://handbook.gitlab.com/handbook/product/personas/#isaac-infrastructure-security-engineer)
|
||||
* [Alex (Security Operations Engineer)](https://handbook.gitlab.com/handbook/product/personas/#alex-security-operations-engineer)
|
||||
* [Cameron (Compliance Manager)](https://handbook.gitlab.com/handbook/product/personas/#cameron-compliance-manager)
|
||||
|
||||
-->
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ Explore (../../doc/development/internal_analytics/internal_event_instrumentation
|
|||
|
||||
### What is the type of buyer?
|
||||
|
||||
<!-- What is the buyer persona for this feature? See https://about.gitlab.com/handbook/product/personas/buyer-persona/
|
||||
<!-- What is the buyer persona for this feature? See https://handbook.gitlab.com/handbook/product/personas/buyer-persona/
|
||||
In which enterprise tier should this feature go? See https://about.gitlab.com/handbook/product/pricing/#three-tiers -->
|
||||
|
||||
### Is this a cross-stage feature?
|
||||
|
|
|
|||
|
|
@ -6,25 +6,25 @@ The goal of this template is to ensure we have captured all the information avai
|
|||
|
||||
<!-- If known, include any of the following: types of users (e.g. Developer), personas, or specific company roles (e.g. Release Manager). It's okay to write "Unknown" and fill this field in later.
|
||||
|
||||
Personas are described at https://about.gitlab.com/handbook/product/personas/
|
||||
Personas are described at https://handbook.gitlab.com/handbook/product/personas/
|
||||
|
||||
* [Parker (Product Manager)](https://about.gitlab.com/handbook/product/personas/#parker-product-manager)
|
||||
* [Delaney (Development Team Lead)](https://about.gitlab.com/handbook/product/personas/#delaney-development-team-lead)
|
||||
* [Presley (Product Designer)](https://about.gitlab.com/handbook/product/personas/#presley-product-designer)
|
||||
* [Sasha (Software Developer)](https://about.gitlab.com/handbook/product/personas/#sasha-software-developer)
|
||||
* [Priyanka (Platform Engineer)](https://about.gitlab.com/handbook/product/personas/#priyanka-platform-engineer)
|
||||
* [Sidney (Systems Administrator)](https://about.gitlab.com/handbook/product/personas/#sidney-systems-administrator)
|
||||
* [Rachel (Release Manager)](https://about.gitlab.com/handbook/product/personas/#rachel-release-manager)
|
||||
* [Simone (Software Engineer in Test)](https://about.gitlab.com/handbook/product/personas/#simone-software-engineer-in-test)
|
||||
* [Allison (Application Ops)](https://about.gitlab.com/handbook/product/personas/#allison-application-ops)
|
||||
* [Ingrid (Infrastructure Operator)](https://about.gitlab.com/handbook/product/personas/#ingrid-infrastructure-operator)
|
||||
* [Dakota (Application Development Director)](https://about.gitlab.com/handbook/product/personas/#dakota-application-development-director)
|
||||
* [Dana (Data Analyst)](https://about.gitlab.com/handbook/product/personas/#dana-data-analyst)
|
||||
* [Eddie (Content Editor)](https://about.gitlab.com/handbook/product/personas/#eddie-content-editor)
|
||||
* [Amy (Application Security Engineer)](https://about.gitlab.com/handbook/product/personas/#amy-application-security-engineer)
|
||||
* [Isaac (Infrastructure Engineer)](https://about.gitlab.com/handbook/product/personas/#isaac-infrastructure-security-engineer)
|
||||
* [Alex (Security Operations Engineer)](https://about.gitlab.com/handbook/product/personas/#alex-security-operations-engineer)
|
||||
* [Cameron (Compliance Manager)](https://about.gitlab.com/handbook/product/personas/#cameron-compliance-manager)
|
||||
* [Parker (Product Manager)](https://handbook.gitlab.com/handbook/product/personas/#parker-product-manager)
|
||||
* [Delaney (Development Team Lead)](https://handbook.gitlab.com/handbook/product/personas/#delaney-development-team-lead)
|
||||
* [Presley (Product Designer)](https://handbook.gitlab.com/handbook/product/personas/#presley-product-designer)
|
||||
* [Sasha (Software Developer)](https://handbook.gitlab.com/handbook/product/personas/#sasha-software-developer)
|
||||
* [Priyanka (Platform Engineer)](https://handbook.gitlab.com/handbook/product/personas/#priyanka-platform-engineer)
|
||||
* [Sidney (Systems Administrator)](https://handbook.gitlab.com/handbook/product/personas/#sidney-systems-administrator)
|
||||
* [Rachel (Release Manager)](https://handbook.gitlab.com/handbook/product/personas/#rachel-release-manager)
|
||||
* [Simone (Software Engineer in Test)](https://handbook.gitlab.com/handbook/product/personas/#simone-software-engineer-in-test)
|
||||
* [Allison (Application Ops)](https://handbook.gitlab.com/handbook/product/personas/#allison-application-ops)
|
||||
* [Ingrid (Infrastructure Operator)](https://handbook.gitlab.com/handbook/product/personas/#ingrid-infrastructure-operator)
|
||||
* [Dakota (Application Development Director)](https://handbook.gitlab.com/handbook/product/personas/#dakota-application-development-director)
|
||||
* [Dana (Data Analyst)](https://handbook.gitlab.com/handbook/product/personas/#dana-data-analyst)
|
||||
* [Eddie (Content Editor)](https://handbook.gitlab.com/handbook/product/personas/#eddie-content-editor)
|
||||
* [Amy (Application Security Engineer)](https://handbook.gitlab.com/handbook/product/personas/#amy-application-security-engineer)
|
||||
* [Isaac (Infrastructure Engineer)](https://handbook.gitlab.com/handbook/product/personas/#isaac-infrastructure-security-engineer)
|
||||
* [Alex (Security Operations Engineer)](https://handbook.gitlab.com/handbook/product/personas/#alex-security-operations-engineer)
|
||||
* [Cameron (Compliance Manager)](https://handbook.gitlab.com/handbook/product/personas/#cameron-compliance-manager)
|
||||
|
||||
-->
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
- [ ] Ensure your UI suggestion align with the [Documentation Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide/)
|
||||
- [ ] Engage ~"Technical Writing". They can help craft a term that best describes the feature(s) you’re proposing.
|
||||
- [ ] Follow the [product development workflow](https://about.gitlab.com/handbook/product-development-flow/#validation-phase-2-problem-validation) validation process to ensure you are solving a well understood problem and that the proposed change is understandable and non-disruptive to users. Navigation-specific research is mandatory for additions or when restructuring.
|
||||
- [ ] Engage the [Foundations Product Manager](https://about.gitlab.com/handbook/product/categories/#foundations-group) for approval. The Foundations DRI (@cdybenko) will work with UX partners in product design, research, and technical writing, as applicable.
|
||||
- [ ] Engage the [Foundations Product Manager](https://about.gitlab.com/handbook/product/categories/#foundations-group) for approval. The Foundations DRI (@jtucker_gl) will work with UX partners in product design, research, and technical writing, as applicable.
|
||||
- [ ] Consider whether you need to [communicate the change somehow](https://design.gitlab.com/patterns/navigation#messaging-changes-to-users), or if you will have an interim period in the UI where your item will live in more than one place.
|
||||
- [ ] Ensure engineers are familiar with the [implementation steps for navigation](https://docs.gitlab.com/ee/development/navigation_sidebar.html#navigation-sidebar).
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ A user story is a requirement for any functionality or feature and follows this
|
|||
|
||||
- _As a `<user role/customer>`, I want to `<JTBD>` so that I can `<achieve a benefit or result>`._
|
||||
|
||||
Please try to include one user story for the main [persona](https://about.gitlab.com/handbook/product/personas/#list-of-user-personas) who needs this feature.
|
||||
Please try to include one user story for the main [persona](https://handbook.gitlab.com/handbook/product/personas/#list-of-user-personas) who needs this feature.
|
||||
-->
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ Learn more about it in the handbook: https://about.gitlab.com/handbook/product-d
|
|||
|
||||
## Reach
|
||||
|
||||
<!-- Please describe who suffers from this problem. Consider referring to our personas, which are described at https://about.gitlab.com/handbook/product/personas/ -->
|
||||
<!-- Please describe who suffers from this problem. Consider referring to our personas, which are described at https://handbook.gitlab.com/handbook/product/personas/ -->
|
||||
|
||||
<!-- Please also quantify the problem's reach using the following values, considering an aggregate across GitLab.com and self-managed:
|
||||
|
||||
|
|
|
|||
|
|
@ -2681,7 +2681,6 @@ Layout/LineLength:
|
|||
- 'lib/gitlab/usage_data.rb'
|
||||
- 'lib/gitlab/usage_data/topology.rb'
|
||||
- 'lib/gitlab/usage_data_counters/base_counter.rb'
|
||||
- 'lib/gitlab/usage_data_counters/editor_unique_counter.rb'
|
||||
- 'lib/gitlab/usage_data_counters/gitlab_cli_activity_unique_counter.rb'
|
||||
- 'lib/gitlab/usage_data_counters/hll_redis_counter.rb'
|
||||
- 'lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb'
|
||||
|
|
|
|||
|
|
@ -4052,7 +4052,6 @@ RSpec/FeatureCategory:
|
|||
- 'spec/lib/gitlab/usage_data_counters/code_review_events_spec.rb'
|
||||
- 'spec/lib/gitlab/usage_data_counters/cycle_analytics_counter_spec.rb'
|
||||
- 'spec/lib/gitlab/usage_data_counters/designs_counter_spec.rb'
|
||||
- 'spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb'
|
||||
- 'spec/lib/gitlab/usage_data_counters/gitlab_cli_activity_unique_counter_spec.rb'
|
||||
- 'spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb'
|
||||
- 'spec/lib/gitlab/usage_data_counters/ipynb_diff_activity_counter_spec.rb'
|
||||
|
|
|
|||
|
|
@ -19,5 +19,4 @@ Style/KeywordParametersOrder:
|
|||
- 'lib/gitlab/email/smime/signer.rb'
|
||||
- 'lib/gitlab/exclusive_lease.rb'
|
||||
- 'lib/gitlab/merge_requests/mergeability/results_store.rb'
|
||||
- 'lib/gitlab/usage_data_counters/editor_unique_counter.rb'
|
||||
- 'lib/microsoft_teams/notifier.rb'
|
||||
|
|
|
|||
|
|
@ -171,14 +171,10 @@ export default {
|
|||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.sastReportsInInlineDiff ||
|
||||
(!this.codequalityReportAvailable && !this.sastReportAvailable)
|
||||
);
|
||||
return !this.codequalityReportAvailable && !this.sastReportAvailable;
|
||||
},
|
||||
update(data) {
|
||||
const { codequalityReportsComparer, sastReport } = data?.project?.mergeRequest || {};
|
||||
|
||||
this.activeProject = data?.project?.mergeRequest?.project;
|
||||
if (
|
||||
(sastReport?.status === FINDINGS_STATUS_PARSED || !this.sastReportAvailable) &&
|
||||
|
|
@ -307,9 +303,6 @@ export default {
|
|||
resourceId() {
|
||||
return convertToGraphQLId('MergeRequest', this.getNoteableData.id);
|
||||
},
|
||||
sastReportsInInlineDiff() {
|
||||
return this.glFeatures.sastReportsInInlineDiff;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
commit(newCommit, oldCommit) {
|
||||
|
|
@ -347,10 +340,6 @@ export default {
|
|||
isLoading: 'adjustView',
|
||||
},
|
||||
mounted() {
|
||||
if (this.endpointCodequality) {
|
||||
this.setCodequalityEndpoint(this.endpointCodequality);
|
||||
}
|
||||
|
||||
if (this.shouldShow) {
|
||||
this.fetchData();
|
||||
}
|
||||
|
|
@ -423,14 +412,12 @@ export default {
|
|||
...mapActions(['startTaskList']),
|
||||
...mapActions('diffs', [
|
||||
'moveToNeighboringCommit',
|
||||
'setCodequalityEndpoint',
|
||||
'fetchDiffFilesMeta',
|
||||
'fetchDiffFilesBatch',
|
||||
'fetchFileByFile',
|
||||
'loadCollapsedDiff',
|
||||
'setFileForcedOpen',
|
||||
'fetchCoverageFiles',
|
||||
'fetchCodequality',
|
||||
'rereadNoteHash',
|
||||
'startRenderDiffsQueue',
|
||||
'assignDiscussionsToDiff',
|
||||
|
|
@ -576,10 +563,6 @@ export default {
|
|||
this.fetchCoverageFiles();
|
||||
}
|
||||
|
||||
if (this.endpointCodequality && !this.sastReportsInInlineDiff) {
|
||||
this.fetchCodequality();
|
||||
}
|
||||
|
||||
if (!this.isNotesFetched) {
|
||||
notesEventHub.$emit('fetchNotesData');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
<script>
|
||||
import DiffInlineFindingsItem from './diff_inline_findings_item.vue';
|
||||
|
||||
export default {
|
||||
components: { DiffInlineFindingsItem },
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
findings: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h4 data-testid="diff-inline-findings-heading" class="gl-my-0 gl-font-base gl-font-regular">
|
||||
{{ title }}
|
||||
</h4>
|
||||
<ul class="gl-list-style-none gl-mb-0 gl-p-0">
|
||||
<diff-inline-findings-item
|
||||
v-for="finding in findings"
|
||||
:key="finding.description"
|
||||
:finding="finding"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
<script>
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import { getSeverity } from '~/ci/reports/utils';
|
||||
|
||||
export default {
|
||||
components: { GlIcon },
|
||||
|
||||
props: {
|
||||
finding: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
enhancedFinding() {
|
||||
return getSeverity(this.finding);
|
||||
},
|
||||
listText() {
|
||||
return `${this.finding.severity} - ${this.finding.description}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li class="gl-py-1 gl-font-regular gl-display-flex">
|
||||
<span class="gl-mr-3">
|
||||
<gl-icon
|
||||
:size="12"
|
||||
:name="enhancedFinding.name"
|
||||
:class="enhancedFinding.class"
|
||||
class="inline-findings-severity-icon"
|
||||
/>
|
||||
</span>
|
||||
<span data-testid="description-plain-text" class="gl-display-flex">
|
||||
{{ listText }}
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
<script>
|
||||
import InlineFindings from './inline_findings.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
InlineFindings,
|
||||
},
|
||||
props: {
|
||||
line: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
parsedCodeQuality() {
|
||||
return (this.line.left ?? this.line.right)?.codequality;
|
||||
},
|
||||
codeQualityLineNumber() {
|
||||
return this.parsedCodeQuality[0]?.line;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
hideInlineFindings() {
|
||||
this.$emit('hideInlineFindings', this.codeQualityLineNumber);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<inline-findings :code-quality="parsedCodeQuality" @hideInlineFindings="hideInlineFindings" />
|
||||
</template>
|
||||
|
|
@ -24,8 +24,8 @@ import * as utils from './diff_row_utils';
|
|||
|
||||
export default {
|
||||
DiffGutterAvatars,
|
||||
InlineFindingsFlagSwitcher: () =>
|
||||
import('ee_component/diffs/components/inline_findings_flag_switcher.vue'),
|
||||
InlineFindingsGutterIconDropdown: () =>
|
||||
import('ee_component/diffs/components/inline_findings_gutter_icon_dropdown.vue'),
|
||||
|
||||
// Temporary mixin for migration from Vue.js 2 to @vue/compat
|
||||
mixins: [compatFunctionalMixin],
|
||||
|
|
@ -80,11 +80,6 @@ export default {
|
|||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
inlineFindingsExpanded: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
classNameMap: memoize(
|
||||
(props) => {
|
||||
|
|
@ -336,9 +331,8 @@ export default {
|
|||
:class="$options.parallelViewLeftLineType(props)"
|
||||
>
|
||||
<component
|
||||
:is="$options.InlineFindingsFlagSwitcher"
|
||||
:is="$options.InlineFindingsGutterIconDropdown"
|
||||
v-if="$options.showCodequalityLeft(props) || $options.showSecurityLeft(props)"
|
||||
:inline-findings-expanded="props.inlineFindingsExpanded"
|
||||
:code-quality="props.line.left.codequality"
|
||||
:sast="props.line.left.sast"
|
||||
:file-path="props.filePath"
|
||||
|
|
@ -478,7 +472,7 @@ export default {
|
|||
:class="$options.classNameMapCellRight(props)"
|
||||
>
|
||||
<component
|
||||
:is="$options.InlineFindingsFlagSwitcher"
|
||||
:is="$options.InlineFindingsGutterIconDropdown"
|
||||
v-if="$options.showCodequalityRight(props) || $options.showSecurityRight(props)"
|
||||
:code-quality="props.line.right.codequality"
|
||||
:sast="props.line.right.sast"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
|
|||
import { hide } from '~/tooltips';
|
||||
import { pickDirection } from '../utils/diff_line';
|
||||
import DiffCommentCell from './diff_comment_cell.vue';
|
||||
import DiffLine from './diff_line.vue';
|
||||
import DiffExpansionCell from './diff_expansion_cell.vue';
|
||||
import DiffRow from './diff_row.vue';
|
||||
import { isHighlighted } from './diff_row_utils';
|
||||
|
|
@ -19,7 +18,6 @@ export default {
|
|||
components: {
|
||||
DiffExpansionCell,
|
||||
DiffRow,
|
||||
DiffLine,
|
||||
DiffCommentCell,
|
||||
DraftNote,
|
||||
},
|
||||
|
|
@ -48,11 +46,6 @@ export default {
|
|||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inlineFindingsExpandedLines: [],
|
||||
};
|
||||
},
|
||||
idState() {
|
||||
return {
|
||||
dragStart: null,
|
||||
|
|
@ -87,9 +80,6 @@ export default {
|
|||
this.sastDiff?.added?.length > 0
|
||||
);
|
||||
},
|
||||
sastReportsInInlineDiff() {
|
||||
return this.glFeatures.sastReportsInInlineDiff;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.onDragOverThrottled = throttle((line) => this.onDragOver(line), 100, { leading: true });
|
||||
|
|
@ -109,19 +99,6 @@ export default {
|
|||
}
|
||||
this.idState.dragStart = line;
|
||||
},
|
||||
hideInlineFindings(line) {
|
||||
const index = this.inlineFindingsExpandedLines.indexOf(line);
|
||||
if (index > -1) {
|
||||
this.inlineFindingsExpandedLines.splice(index, 1);
|
||||
}
|
||||
},
|
||||
toggleCodeQualityFindings(line) {
|
||||
if (!this.inlineFindingsExpandedLines.includes(line)) {
|
||||
this.inlineFindingsExpandedLines.push(line);
|
||||
} else {
|
||||
this.hideInlineFindings(line);
|
||||
}
|
||||
},
|
||||
onDragOver(line) {
|
||||
if (line.chunk !== this.idState.dragStart.chunk) return;
|
||||
|
||||
|
|
@ -266,12 +243,10 @@ export default {
|
|||
:is-last-highlighted-line="isLastHighlightedLine(line) || index === commentedLines.endLine"
|
||||
:inline="inline"
|
||||
:index="index"
|
||||
:inline-findings-expanded="inlineFindingsExpandedLines.includes(getCodeQualityLine(line))"
|
||||
:file-line-coverage="fileLineCoverage"
|
||||
:coverage-loaded="coverageLoaded"
|
||||
@showCommentForm="(code) => singleLineComment(code, line)"
|
||||
@setHighlightedRow="setHighlightedRow"
|
||||
@toggleCodeQualityFindings="toggleCodeQualityFindings"
|
||||
@toggleLineDiscussions="
|
||||
({ lineCode, expanded }) =>
|
||||
toggleLineDiscussions({ lineCode, fileHash: diffFile.file_hash, expanded })
|
||||
|
|
@ -280,15 +255,6 @@ export default {
|
|||
@startdragging="onStartDragging"
|
||||
@stopdragging="onStopDragging"
|
||||
/>
|
||||
<!-- Don't display InlineFindings expanded section when sastReportsInInlineDiff is false -->
|
||||
<diff-line
|
||||
v-if="
|
||||
inlineFindingsExpandedLines.includes(getCodeQualityLine(line)) && !sastReportsInInlineDiff
|
||||
"
|
||||
:key="line.line_code"
|
||||
:line="line"
|
||||
@hideInlineFindings="hideInlineFindings"
|
||||
/>
|
||||
<div
|
||||
v-if="line.renderCommentRow"
|
||||
:key="`dcr-${line.line_code || index}`"
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
<script>
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { NEW_CODE_QUALITY_FINDINGS, NEW_SAST_FINDINGS } from '../i18n';
|
||||
import DiffInlineFindings from './diff_inline_findings.vue';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
newCodeQualityFindings: NEW_CODE_QUALITY_FINDINGS,
|
||||
newSastFindings: NEW_SAST_FINDINGS,
|
||||
},
|
||||
components: { GlButton, DiffInlineFindings },
|
||||
props: {
|
||||
codeQuality: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-testid="inline-findings"
|
||||
class="gl-relative inline-findings-list gl-border-top-1 gl-border-bottom-1 gl-bg-gray-10 gl-text-black-normal gl-pl-5 gl-pt-4 gl-pb-4"
|
||||
>
|
||||
<diff-inline-findings
|
||||
v-if="codeQuality.length"
|
||||
:title="$options.i18n.newCodeQualityFindings"
|
||||
:findings="codeQuality"
|
||||
/>
|
||||
|
||||
<gl-button
|
||||
data-testid="inline-findings-close"
|
||||
category="tertiary"
|
||||
size="small"
|
||||
icon="close"
|
||||
class="gl-absolute gl-right-2 gl-top-2"
|
||||
@click="$emit('hideInlineFindings')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -34,7 +34,6 @@ export default function initDiffsApp(store = notesStore) {
|
|||
projectPath: dataset.projectPath || '',
|
||||
iid: dataset.iid || '',
|
||||
endpointCoverage: dataset.endpointCoverage || '',
|
||||
endpointCodequality: dataset.endpointCodequality || '',
|
||||
codequalityReportAvailable: parseBoolean(dataset.codequalityReportAvailable),
|
||||
sastReportAvailable: parseBoolean(dataset.sastReportAvailable),
|
||||
helpPagePath: dataset.helpPagePath,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as actions from 'ee_else_ce/diffs/store/actions';
|
||||
import * as getters from 'ee_else_ce/diffs/store/getters';
|
||||
import createState from 'ee_else_ce/diffs/store/modules/diff_state';
|
||||
import mutations from 'ee_else_ce/diffs/store/mutations';
|
||||
import * as actions from '../actions';
|
||||
|
||||
export default () => ({
|
||||
namespaced: true,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlSorting, GlSortingItem } from '@gitlab/ui';
|
||||
import { GlSorting } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState } from 'vuex';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
|
|
@ -9,7 +9,7 @@ import { SORT_DIRECTION_UI } from '~/search/sort/constants';
|
|||
|
||||
export default {
|
||||
name: 'SortDropdown',
|
||||
components: { GlSorting, GlSortingItem },
|
||||
components: { GlSorting },
|
||||
inject: ['namespace'],
|
||||
computed: {
|
||||
...mapState({
|
||||
|
|
@ -29,6 +29,9 @@ export default {
|
|||
activeOptionLabel() {
|
||||
return this.activeOption?.label;
|
||||
},
|
||||
activeOptionKey() {
|
||||
return this.activeOption?.key;
|
||||
},
|
||||
isAscending() {
|
||||
return !this.sort.sortDesc;
|
||||
},
|
||||
|
|
@ -39,14 +42,8 @@ export default {
|
|||
return FIELDS.filter(
|
||||
(field) => this.tableSortableFields.includes(field.key) && field.sort,
|
||||
).map((field) => ({
|
||||
key: field.key,
|
||||
label: field.label,
|
||||
href: buildSortHref({
|
||||
sortBy: field.key,
|
||||
sortDesc: false,
|
||||
filteredSearchBarTokens: this.filteredSearchBar.tokens,
|
||||
filteredSearchBarSearchParam: this.filteredSearchBar.searchParam,
|
||||
}),
|
||||
text: field.label,
|
||||
value: field.key,
|
||||
}));
|
||||
},
|
||||
},
|
||||
|
|
@ -64,6 +61,16 @@ export default {
|
|||
}),
|
||||
);
|
||||
},
|
||||
handleSortingItemClick(value) {
|
||||
visitUrl(
|
||||
buildSortHref({
|
||||
sortBy: value,
|
||||
sortDesc: false,
|
||||
filteredSearchBarTokens: this.filteredSearchBar.tokens,
|
||||
filteredSearchBarSearchParam: this.filteredSearchBar.searchParam,
|
||||
}),
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -76,15 +83,9 @@ export default {
|
|||
:text="activeOptionLabel"
|
||||
:is-ascending="isAscending"
|
||||
:sort-direction-tool-tip="sortDirectionData.tooltip"
|
||||
:sort-options="filteredOptions"
|
||||
:sort-by="activeOptionKey"
|
||||
@sortByChange="handleSortingItemClick"
|
||||
@sortDirectionChange="handleSortDirectionChange"
|
||||
>
|
||||
<gl-sorting-item
|
||||
v-for="option in filteredOptions"
|
||||
:key="option.key"
|
||||
:href="option.href"
|
||||
:active="isActive(option.key)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</gl-sorting-item>
|
||||
</gl-sorting>
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -127,5 +127,6 @@ export default {
|
|||
</gl-sprintf>
|
||||
</span>
|
||||
</p>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
before_action only: [:show, :diffs] do
|
||||
push_frontend_feature_flag(:core_security_mr_widget_counts, project)
|
||||
push_frontend_feature_flag(:moved_mr_sidebar, project)
|
||||
push_frontend_feature_flag(:sast_reports_in_inline_diff, project)
|
||||
push_frontend_feature_flag(:mr_experience_survey, project)
|
||||
push_frontend_feature_flag(:ci_job_failures_in_mr, project)
|
||||
push_frontend_feature_flag(:mr_pipelines_graphql, project)
|
||||
|
|
|
|||
|
|
@ -87,15 +87,15 @@ class ProjectsController < Projects::ApplicationController
|
|||
|
||||
@parent_group = Group.find_by(id: params[:namespace_id])
|
||||
|
||||
manageable_groups_count = current_user.manageable_groups(include_groups_with_developer_maintainer_access: true).count
|
||||
manageable_groups = ::Groups::AcceptingProjectCreationsFinder.new(current_user).execute.limit(2)
|
||||
|
||||
if manageable_groups_count == 0 && !can?(current_user, :create_projects, current_user.namespace)
|
||||
if manageable_groups.empty? && !can?(current_user, :create_projects, current_user.namespace)
|
||||
return access_denied!
|
||||
end
|
||||
|
||||
@current_user_group =
|
||||
if manageable_groups_count == 1
|
||||
current_user.manageable_groups(include_groups_with_developer_maintainer_access: true).first
|
||||
if manageable_groups.one?
|
||||
manageable_groups.first
|
||||
end
|
||||
|
||||
@project = Project.new(namespace_id: @namespace&.id)
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ module Groups
|
|||
[
|
||||
current_user
|
||||
.manageable_groups(include_groups_with_developer_maintainer_access: true)
|
||||
.project_creation_allowed,
|
||||
.project_creation_allowed(current_user),
|
||||
owner_maintainer_groups_originating_from_group_shares
|
||||
.project_creation_allowed,
|
||||
.project_creation_allowed(current_user),
|
||||
*developer_groups_originating_from_group_shares
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ module Groups
|
|||
|
||||
groups = ::Group.from_union(groups_accepting_project_transfers)
|
||||
|
||||
groups.project_creation_allowed
|
||||
groups.project_creation_allowed(current_user)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -53,7 +53,11 @@ module Mutations
|
|||
|
||||
# Only when the user is not an api user and the operation was successful
|
||||
if !api_user? && service_response.success?
|
||||
::Gitlab::UsageDataCounters::EditorUniqueCounter.track_snippet_editor_edit_action(author: current_user, project: project)
|
||||
Gitlab::InternalEvents.track_event(
|
||||
'g_edit_by_snippet_ide',
|
||||
user: current_user,
|
||||
project: project
|
||||
)
|
||||
end
|
||||
|
||||
snippet = service_response.payload[:snippet]
|
||||
|
|
|
|||
|
|
@ -42,7 +42,11 @@ module Mutations
|
|||
|
||||
# Only when the user is not an api user and the operation was successful
|
||||
if !api_user? && service_response.success?
|
||||
::Gitlab::UsageDataCounters::EditorUniqueCounter.track_snippet_editor_edit_action(author: current_user, project: snippet.project)
|
||||
Gitlab::InternalEvents.track_event(
|
||||
'g_edit_by_snippet_ide',
|
||||
user: current_user,
|
||||
project: snippet.project
|
||||
)
|
||||
end
|
||||
|
||||
snippet = service_response.payload[:snippet]
|
||||
|
|
|
|||
|
|
@ -211,7 +211,11 @@ class Group < Namespace
|
|||
where(project_creation_level: project_creation_levels)
|
||||
end
|
||||
|
||||
scope :project_creation_allowed, -> do
|
||||
scope :excluding_restricted_visibility_levels_for_user, -> (user) do
|
||||
user.can_admin_all_resources? ? all : where.not(visibility_level: Gitlab::CurrentSettings.restricted_visibility_levels)
|
||||
end
|
||||
|
||||
scope :project_creation_allowed, -> (user) do
|
||||
project_creation_allowed_on_levels = [
|
||||
::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS,
|
||||
::Gitlab::Access::MAINTAINER_PROJECT_ACCESS,
|
||||
|
|
@ -227,7 +231,7 @@ class Group < Namespace
|
|||
project_creation_allowed_on_levels.delete(nil)
|
||||
end
|
||||
|
||||
with_project_creation_levels(project_creation_allowed_on_levels)
|
||||
with_project_creation_levels(project_creation_allowed_on_levels).excluding_restricted_visibility_levels_for_user(user)
|
||||
end
|
||||
|
||||
scope :shared_into_ancestors, -> (group) do
|
||||
|
|
|
|||
|
|
@ -2,12 +2,21 @@
|
|||
|
||||
module Projects
|
||||
class BranchRule
|
||||
include GlobalID::Identification
|
||||
extend Forwardable
|
||||
|
||||
attr_reader :project, :protected_branch
|
||||
alias_method :branch_protection, :protected_branch
|
||||
|
||||
def_delegators(:protected_branch, :name, :group, :default_branch?, :created_at, :updated_at)
|
||||
def_delegators(:protected_branch, :id, :name, :group, :default_branch?, :created_at, :updated_at)
|
||||
|
||||
def self.find(id)
|
||||
protected_branch = ProtectedBranch.find(id)
|
||||
|
||||
new(protected_branch.project, protected_branch)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
raise ActiveRecord::RecordNotFound, "Couldn't find Projects::BranchRule with 'id'=#{id}"
|
||||
end
|
||||
|
||||
def initialize(project, protected_branch)
|
||||
@project = project
|
||||
|
|
|
|||
|
|
@ -2,41 +2,16 @@
|
|||
= content_for :head do
|
||||
= stylesheet_link_tag 'mailers/highlighted_diff_email'
|
||||
|
||||
- if Feature.enabled?(:enhanced_review_email, @project, type: :gitlab_com_derisk)
|
||||
%div{ style: "color:#333333;border-bottom:8px solid #ededed;font-weight:bold;line-height:1.4;padding: 20px 0;" }
|
||||
- mr_link = link_to(@merge_request.to_reference(@project), project_merge_request_url(@project, @merge_request))
|
||||
- mr_author_link = link_to(@author_name, user_url(@author))
|
||||
= _('Merge request %{mr_link} was reviewed by %{mr_author}').html_safe % { mr_link: mr_link, mr_author: mr_author_link }
|
||||
%div{ style: "color:#333333;border-bottom:8px solid #ededed;font-weight:bold;line-height:1.4;padding: 20px 0;" }
|
||||
- mr_link = link_to(@merge_request.to_reference(@project), project_merge_request_url(@project, @merge_request))
|
||||
- mr_author_link = link_to(@author_name, user_url(@author))
|
||||
= _('Merge request %{mr_link} was reviewed by %{mr_author}').html_safe % { mr_link: mr_link, mr_author: mr_author_link }
|
||||
|
||||
- @notes.each do |note|
|
||||
-# Get preloaded note discussion
|
||||
- discussion = @discussions[note.discussion_id] if note.part_of_discussion?
|
||||
-# Preload project for discussions first note
|
||||
- discussion.first_note.project = @project if discussion&.first_note
|
||||
- target_url = project_merge_request_url(@project, @merge_request, anchor: "note_#{note.id}")
|
||||
= render 'note_email', note: note, diff_limit: 3, target_url: target_url, note_style: "border-bottom:4px solid #ededed; padding-bottom: 1em;", include_stylesheet_link: false, discussion: discussion, author: @author
|
||||
= render_if_exists 'notify/review_summary'
|
||||
|
||||
- else
|
||||
|
||||
%table{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;margin:0 auto;border-collapse:separate;border-spacing:0;" }
|
||||
%tbody
|
||||
%tr
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;overflow:hidden;" }
|
||||
%table{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" }
|
||||
%tbody
|
||||
%tr
|
||||
%td{ style: "color:#333333;border-bottom:1px solid #ededed;font-weight:bold;line-height:1.4;padding: 20px 0;" }
|
||||
- mr_link = link_to(@merge_request.to_reference(@project), project_merge_request_url(@project, @merge_request))
|
||||
- mr_author_link = link_to(@author_name, user_url(@author))
|
||||
= _('Merge request %{mr_link} was reviewed by %{mr_author}').html_safe % { mr_link: mr_link, mr_author: mr_author_link }
|
||||
%tr
|
||||
%td{ style: "overflow:hidden;line-height:1.4;display:grid;" }
|
||||
- @notes.each do |note|
|
||||
-# Get preloaded note discussion
|
||||
- discussion = @discussions[note.discussion_id] if note.part_of_discussion?
|
||||
-# Preload project for discussions first note
|
||||
- discussion.first_note.project = @project if discussion&.first_note
|
||||
- target_url = project_merge_request_url(@project, @merge_request, anchor: "note_#{note.id}")
|
||||
= render 'note_email', note: note, diff_limit: 3, target_url: target_url, note_style: "border-bottom:1px solid #ededed; padding-bottom: 1em;", include_stylesheet_link: false, discussion: discussion, author: @author
|
||||
= render_if_exists 'notify/review_summary'
|
||||
- @notes.each do |note|
|
||||
-# Get preloaded note discussion
|
||||
- discussion = @discussions[note.discussion_id] if note.part_of_discussion?
|
||||
-# Preload project for discussions first note
|
||||
- discussion.first_note.project = @project if discussion&.first_note
|
||||
- target_url = project_merge_request_url(@project, @merge_request, anchor: "note_#{note.id}")
|
||||
= render 'note_email', note: note, diff_limit: 3, target_url: target_url, note_style: "border-bottom:4px solid #ededed; padding-bottom: 1em;", include_stylesheet_link: false, discussion: discussion, author: @author
|
||||
= render_if_exists 'notify/review_summary'
|
||||
|
|
|
|||
|
|
@ -83,13 +83,13 @@ module ClickHouse
|
|||
|
||||
paths = id_paths.map(&:second).map { |value| "'#{value}'" }.join(',')
|
||||
query = ClickHouse::Client::Query.new(
|
||||
raw_query: "DELETE FROM events WHERE path IN (#{paths})"
|
||||
raw_query: "ALTER TABLE events DELETE WHERE path IN (#{paths})"
|
||||
)
|
||||
|
||||
connection.execute(query)
|
||||
|
||||
query = ClickHouse::Client::Query.new(
|
||||
raw_query: 'DELETE FROM event_namespace_paths WHERE namespace_id IN ({namespace_ids:Array(UInt64)})',
|
||||
raw_query: 'ALTER TABLE event_namespace_paths DELETE WHERE namespace_id IN ({namespace_ids:Array(UInt64)})',
|
||||
placeholders: { namespace_ids: id_paths.map(&:first).to_json }
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
---
|
||||
name: enhanced_review_email
|
||||
feature_issue_url:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/141187
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/437582
|
||||
milestone: '16.8'
|
||||
group: group::code review
|
||||
type: gitlab_com_derisk
|
||||
name: redis_hll_property_name_tracking
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137890
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/432866
|
||||
milestone: '16.9'
|
||||
type: wip
|
||||
group: group::analytics instrumentation
|
||||
default_enabled: false
|
||||
|
|
@ -29,6 +29,7 @@ Redis::Cluster.prepend(Gitlab::Patch::RedisCluster)
|
|||
|
||||
# this only instruments `RedisClient` used in `Sidekiq.redis`
|
||||
RedisClient.register(Gitlab::Instrumentation::RedisClientMiddleware)
|
||||
RedisClient.prepend(Gitlab::Patch::RedisClient)
|
||||
|
||||
if Gitlab::Redis::Workhorse.params[:cluster].present?
|
||||
raise "Do not configure workhorse with a Redis Cluster as pub/sub commands are not cluster-compatible."
|
||||
|
|
|
|||
|
|
@ -1,7 +1,16 @@
|
|||
---
|
||||
table_name: catalog_resource_components
|
||||
feature_categories:
|
||||
- pipeline_composition
|
||||
description: CI component available in the CI Catalog
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127775
|
||||
milestone: '16.3'
|
||||
gitlab_schema: gitlab_main
|
||||
gitlab_schema: gitlab_main_cell
|
||||
allow_cross_joins:
|
||||
- gitlab_main_clusterwide
|
||||
allow_cross_transactions:
|
||||
- gitlab_main_clusterwide
|
||||
allow_cross_foreign_keys:
|
||||
- gitlab_main_clusterwide
|
||||
sharding_key:
|
||||
project_id: projects
|
||||
|
|
|
|||
|
|
@ -5,4 +5,12 @@ feature_categories:
|
|||
description: Catalog resource versions that contain valid CI components.
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124668
|
||||
milestone: '16.2'
|
||||
gitlab_schema: gitlab_main
|
||||
gitlab_schema: gitlab_main_cell
|
||||
allow_cross_joins:
|
||||
- gitlab_main_clusterwide
|
||||
allow_cross_transactions:
|
||||
- gitlab_main_clusterwide
|
||||
allow_cross_foreign_keys:
|
||||
- gitlab_main_clusterwide
|
||||
sharding_key:
|
||||
project_id: projects
|
||||
|
|
|
|||
|
|
@ -5,4 +5,12 @@ feature_categories:
|
|||
description: Projects containing a catalog resource.
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112482
|
||||
milestone: '15.10'
|
||||
gitlab_schema: gitlab_main
|
||||
gitlab_schema: gitlab_main_cell
|
||||
allow_cross_joins:
|
||||
- gitlab_main_clusterwide
|
||||
allow_cross_transactions:
|
||||
- gitlab_main_clusterwide
|
||||
allow_cross_foreign_keys:
|
||||
- gitlab_main_clusterwide
|
||||
sharding_key:
|
||||
project_id: projects
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveIndexProtectedEnvironmentsOnProjectId < Gitlab::Database::Migration[2.2]
|
||||
disable_ddl_transaction!
|
||||
|
||||
milestone '16.9'
|
||||
|
||||
INDEX_NAME = 'index_protected_environments_on_project_id'
|
||||
|
||||
def up
|
||||
remove_concurrent_index_by_name :protected_environments, name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index :protected_environments, :project_id, name: INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
1becea9a8277072f40d0fa7325338ae92e698b39e7c84b1be8ffe52832ef6794
|
||||
|
|
@ -35115,8 +35115,6 @@ CREATE INDEX index_protected_environments_on_approval_count_and_created_at ON pr
|
|||
|
||||
CREATE UNIQUE INDEX index_protected_environments_on_group_id_and_name ON protected_environments USING btree (group_id, name) WHERE (group_id IS NOT NULL);
|
||||
|
||||
CREATE INDEX index_protected_environments_on_project_id ON protected_environments USING btree (project_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_protected_environments_on_project_id_and_name ON protected_environments USING btree (project_id, name);
|
||||
|
||||
CREATE INDEX index_protected_tag_create_access ON protected_tag_create_access_levels USING btree (protected_tag_id);
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 34 KiB |
|
|
@ -26,7 +26,7 @@ You can use the [Web IDE](../../../user/project/web_ide/index.md) to commit chan
|
|||
|
||||
## Long-term vision
|
||||
|
||||
As a [new Software Developer to a team such as Sasha](https://about.gitlab.com/handbook/product/personas/#sasha-software-developer) with no local development environment, I should be able to:
|
||||
As a [new Software Developer to a team such as Sasha](https://handbook.gitlab.com/handbook/product/personas/#sasha-software-developer) with no local development environment, I should be able to:
|
||||
|
||||
- Go to a repository on GitLab.com or self-managed.
|
||||
- Click a button that will provide a list of current workspaces for this repository.
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 340 KiB After Width: | Height: | Size: 116 KiB |
|
|
@ -1998,10 +1998,10 @@ to select a specific site profile and scanner profile.
|
|||
### `dependencies`
|
||||
|
||||
Use the `dependencies` keyword to define a list of specific jobs to fetch [artifacts](#artifacts)
|
||||
from. When `dependencies` is not defined in a job, all jobs in earlier stages are considered dependent
|
||||
and the job fetches all artifacts from those jobs.
|
||||
from. The specified jobs must all be in earlier stages. You can also set a job to download no artifacts at all.
|
||||
|
||||
You can also set a job to download no artifacts at all.
|
||||
When `dependencies` is not defined in a job, all jobs in earlier stages are considered dependent
|
||||
and the job fetches all artifacts from those jobs.
|
||||
|
||||
**Keyword type**: Job keyword. You can use it only as part of a job.
|
||||
|
||||
|
|
@ -2057,6 +2057,8 @@ the [stage](#stages) precedence.
|
|||
- The job status does not matter. If a job fails or it's a manual job that isn't triggered, no error occurs.
|
||||
- If the artifacts of a dependent job are [expired](#artifactsexpire_in) or
|
||||
[deleted](../jobs/job_artifacts.md#delete-job-log-and-artifacts), then the job fails.
|
||||
- To fetch artifacts from a job in the same stage, you must use [`needs:artifacts`](#needsartifacts).
|
||||
You should not combine `dependencies` with `needs` in the same job.
|
||||
|
||||
### `environment`
|
||||
|
||||
|
|
@ -2917,8 +2919,7 @@ In this example:
|
|||
|
||||
**Additional details**:
|
||||
|
||||
- In GitLab 12.6 and later, you can't combine the [`dependencies`](#dependencies) keyword
|
||||
with `needs`.
|
||||
- You should not combine `needs` with [`dependencies`](#dependencies) in the same job.
|
||||
|
||||
#### `needs:project` **(PREMIUM ALL)**
|
||||
|
||||
|
|
|
|||
|
|
@ -199,22 +199,16 @@ In this example, I found some UI text I'd like to change.
|
|||
In the upper-right corner in GitLab, I selected my avatar and then **Preferences**.
|
||||
I want to change this text:
|
||||
|
||||

|
||||

|
||||
|
||||
Other settings on the page start with the word `Customize` and skip the `This setting allows you to` part.
|
||||
I'll update this phrase to match the others.
|
||||
Let's clarify `Customize the color of GitLab.` by changing the phrase to `Customize the color theme of the GitLab UI.`.
|
||||
|
||||
NOTE:
|
||||
As this text has already been [changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/116472) when developing this tutorial, you can instead search for `Customize the appearance of the syntax` to find the files that were changed.
|
||||
1. Search the `gitlab-development-kit/gitlab` directory for the string `Customize the color`.
|
||||
|
||||
1. Search the `gitlab-development-kit/gitlab` directory for the string `This setting allows you to customize`.
|
||||
The results shows 1 `.haml` file, and several `.po` files.
|
||||
|
||||
The results show one `.haml` file, two `.md` files, one `.pot` file, and
|
||||
several `.po` files.
|
||||
|
||||
1. Open the `.haml` file. This file is where the UI text resides.
|
||||
1. Update the string. In this case, I'll remove the words before `customize`
|
||||
and start the word `customize` with a capital `C`.
|
||||
1. Open the `app/views/profiles/preferences/show.html.haml` file. This file is where the UI text resides.
|
||||
1. Update the string from `Customize the color of GitLab.` to `Customize the color theme of the GitLab UI.`.
|
||||
1. Save the file.
|
||||
|
||||
You can check that you were successful:
|
||||
|
|
@ -229,7 +223,7 @@ You can check that you were successful:
|
|||
- Refresh the web browser where you're viewing the GDK.
|
||||
The changes should be displayed. Take a screenshot.
|
||||
|
||||

|
||||

|
||||
|
||||
### Update the translation files
|
||||
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
|
@ -90,7 +90,7 @@ This chart contains several types of items:
|
|||
AB["batch_comments~~draft_note.vue"]
|
||||
AC["diffs~~diff_comment_cell.vue"]
|
||||
AD["diffs~~diff_gutter_avatars.vue"]
|
||||
AE["ee-diffs~~inline_findings_flag_switcher.vue"]
|
||||
AE["ee-diffs~~inline_findings_gutter_icon_dropdown.vue"]
|
||||
AF["notes~~noteable_note.vue"]
|
||||
AG["notes~~note_actions.vue"]
|
||||
AH["notes~~note_body.vue"]
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ GitLab is creating AI-assisted features across our DevSecOps platform. These fea
|
|||
| Helps you remediate vulnerabilities more efficiently, boost your skills, and write more secure code. <br><br><i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=6sDf73QOav8) | [Vulnerability summary](application_security/vulnerabilities/index.md#explaining-a-vulnerability) | **(ULTIMATE SAAS BETA)** |
|
||||
| Generates a merge request containing the changes required to mitigate a vulnerability. | [Vulnerability resolution](application_security/vulnerabilities/index.md#explaining-a-vulnerability) | **(ULTIMATE SAAS EXPERIMENT)** |
|
||||
| Helps you understand code by explaining it in English language. <br><br><i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=1izKaLmmaCA) | [Code explanation](#explain-code-in-the-web-ui-with-code-explanation) | **(ULTIMATE SAAS EXPERIMENT)** |
|
||||
| Processes and generates text and code in a conversational manner. Helps you quickly identify useful information in large volumes of text in issues, epics, code, and GitLab documentation. | [GitLab Duo Chat](gitlab_duo_chat.md) | **(ULTIMATE BETA)** |
|
||||
| Processes and generates text and code in a conversational manner. Helps you quickly identify useful information in large volumes of text in issues, epics, code, and GitLab documentation. | [GitLab Duo Chat](gitlab_duo_chat.md) | **(ULTIMATE ALL BETA)** |
|
||||
| Assists you in determining the root cause for a pipeline failure and failed CI/CD build. | [Root cause analysis](#root-cause-analysis) | **(ULTIMATE SAAS EXPERIMENT)** |
|
||||
| Assists you with predicting productivity metrics and identifying anomalies across your software development lifecycle. | [Value stream forecasting](#forecast-deployment-frequency-with-value-stream-forecasting) | **(ULTIMATE ALL EXPERIMENT)** |
|
||||
| Processes and responds to your questions about your application's usage data. | [Product Analytics](product_analytics/index.md) | **(ULTIMATE SAAS EXPERIMENT)** |
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Deploy
|
||||
group: Environments
|
||||
info: A tutorial for deploying a GitLab repository using Flux
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Tutorial: Deploy a Git repository using Flux **(FREE ALL)**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Deploy
|
||||
group: Environments
|
||||
info: A tutorial for deploying an OCI artifact using Flux
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Tutorial: Deploy an OCI artifact using Flux **(FREE ALL)**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Deploy
|
||||
group: Environments
|
||||
info: A tutorial using Flux
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Tutorial: Set up Flux for GitOps **(FREE ALL)**
|
||||
|
|
|
|||
|
|
@ -63,9 +63,9 @@ GitLab in a Kubernetes cluster, you might need a different version of Kubernetes
|
|||
You can upgrade your
|
||||
Kubernetes version to a supported version at any time:
|
||||
|
||||
- 1.29 (support ends when GitLab version 17.10 is released or when 1.32 becomes supported)
|
||||
- 1.28 (support ends when GitLab version 17.5 is released or when 1.31 becomes supported)
|
||||
- 1.27 (support ends when GitLab version 17.2 is released or when 1.30 becomes supported)
|
||||
- 1.26 (support ends when GitLab version 16.10 is released or when 1.29 becomes supported)
|
||||
|
||||
GitLab aims to support a new minor Kubernetes version three months after its initial release. GitLab supports at least three production-ready Kubernetes minor
|
||||
versions at any given time.
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ To change the default collapse behavior for a file type:
|
|||
docs/** gitlab-generated
|
||||
|
||||
# Do not collapse package-lock.json
|
||||
package-json -gitlab-generated
|
||||
package-lock.json -gitlab-generated
|
||||
```
|
||||
|
||||
1. Commit, push, and merge your changes into your default branch.
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ module API
|
|||
return unless find_user_from_warden
|
||||
|
||||
Gitlab::UsageDataCounters::WebIdeCounter.increment_commits_count
|
||||
Gitlab::UsageDataCounters::EditorUniqueCounter.track_web_ide_edit_action(author: current_user, project: user_project)
|
||||
Gitlab::InternalEvents.track_event('g_edit_by_web_ide', user: current_user, project: user_project)
|
||||
|
||||
namespace = user_project.namespace
|
||||
|
||||
Gitlab::Tracking.event(
|
||||
|
|
|
|||
|
|
@ -16,13 +16,17 @@ module Gitlab
|
|||
|
||||
yield
|
||||
rescue ::Redis::BaseError, ::RedisClient::Error => ex
|
||||
if ex.message.start_with?('MOVED', 'ASK')
|
||||
instrumentation_class.instance_count_cluster_redirection(ex)
|
||||
else
|
||||
instrumentation_class.instance_count_exception(ex)
|
||||
Thread.current[:redis_client_error_count] ||= 0
|
||||
|
||||
# skip instrumentation if the error is a connection error happening for the first time as instrumentation
|
||||
# middlewares are called within `ensure_connected` blocks. Connection retries are not known to the middleware.
|
||||
# Refer to https://github.com/redis-rb/redis-client/issues/119#issuecomment-1829703792
|
||||
unless ex.is_a?(::RedisClient::ConnectionError) && Thread.current[:redis_client_error_count] == 0
|
||||
instrument_errors(instrumentation_class, ex)
|
||||
end
|
||||
|
||||
instrumentation_class.log_exception(ex)
|
||||
Thread.current[:redis_client_error_count] += 1 if ex.is_a?(::RedisClient::Error)
|
||||
|
||||
raise ex
|
||||
ensure
|
||||
duration = Gitlab::Metrics::System.monotonic_time - start
|
||||
|
|
@ -80,6 +84,18 @@ module Gitlab
|
|||
def exclude_from_apdex?(commands)
|
||||
commands.any? { |command| APDEX_EXCLUDE.include?(command.first.to_s.downcase) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def instrument_errors(instrumentation_class, error)
|
||||
if error.message.start_with?('MOVED', 'ASK')
|
||||
instrumentation_class.instance_count_cluster_redirection(error)
|
||||
else
|
||||
instrumentation_class.instance_count_exception(error)
|
||||
end
|
||||
|
||||
instrumentation_class.log_exception(error)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Patch
|
||||
module RedisClient
|
||||
# This patch resets the connection error tracker after each call to prevent state leak
|
||||
# across calls and requests.
|
||||
#
|
||||
# The purpose of the tracker is to silence RedisClient::ConnectionErrors during reconnection attempts.
|
||||
# More details found in https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/2564#note_1665334335
|
||||
def ensure_connected(retryable: true)
|
||||
super
|
||||
ensure
|
||||
Thread.current[:redis_client_error_count] = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module UsageDataCounters
|
||||
module EditorUniqueCounter
|
||||
EDIT_BY_SNIPPET_EDITOR = 'g_edit_by_snippet_ide'
|
||||
EDIT_BY_SFE = 'g_edit_by_sfe'
|
||||
EDIT_BY_WEB_IDE = 'g_edit_by_web_ide'
|
||||
EDIT_CATEGORY = 'ide_edit'
|
||||
|
||||
class << self
|
||||
def track_web_ide_edit_action(author:, project:)
|
||||
track_internal_event(EDIT_BY_WEB_IDE, author, project)
|
||||
end
|
||||
|
||||
def count_web_ide_edit_actions(date_from:, date_to:)
|
||||
count_unique(EDIT_BY_WEB_IDE, date_from, date_to)
|
||||
end
|
||||
|
||||
def track_sfe_edit_action(author:, project:)
|
||||
track_internal_event(EDIT_BY_SFE, author, project)
|
||||
end
|
||||
|
||||
def count_sfe_edit_actions(date_from:, date_to:)
|
||||
count_unique(EDIT_BY_SFE, date_from, date_to)
|
||||
end
|
||||
|
||||
def track_snippet_editor_edit_action(author:, project:)
|
||||
track_internal_event(EDIT_BY_SNIPPET_EDITOR, author, project)
|
||||
end
|
||||
|
||||
def count_snippet_editor_edit_actions(date_from:, date_to:)
|
||||
count_unique(EDIT_BY_SNIPPET_EDITOR, date_from, date_to)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def track_internal_event(event_name, author, project = nil)
|
||||
return unless author
|
||||
|
||||
Gitlab::InternalEvents.track_event(
|
||||
event_name,
|
||||
user: author,
|
||||
project: project,
|
||||
namespace: project&.namespace
|
||||
)
|
||||
end
|
||||
|
||||
def count_unique(actions, date_from, date_to)
|
||||
Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: actions, start_date: date_from, end_date: date_to)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -29,18 +29,20 @@ module Gitlab
|
|||
#
|
||||
# event_name - The event name.
|
||||
# values - One or multiple values counted.
|
||||
# property_name - Name of the values counted.
|
||||
# time - Time of the action, set to Time.current.
|
||||
def track_event(event_name, values:, time: Time.current)
|
||||
track(values, event_name, time: time)
|
||||
def track_event(event_name, values:, property_name: nil, time: Time.current)
|
||||
track(values, event_name, property_name: property_name, time: time)
|
||||
end
|
||||
|
||||
# Count unique events for a given time range.
|
||||
#
|
||||
# event_names - The list of the events to count.
|
||||
# property_names - The list of the values for which the events are to be counted.
|
||||
# start_date - The start date of the time range.
|
||||
# end_date - The end date of the time range.
|
||||
def unique_events(event_names:, start_date:, end_date:)
|
||||
count_unique_events(event_names: event_names, start_date: start_date, end_date: end_date)
|
||||
def unique_events(event_names:, start_date:, end_date:, property_name: nil)
|
||||
count_unique_events(event_names: event_names, property_name: property_name, start_date: start_date, end_date: end_date)
|
||||
end
|
||||
|
||||
def known_event?(event_name)
|
||||
|
|
@ -52,19 +54,19 @@ module Gitlab
|
|||
end
|
||||
|
||||
def calculate_events_union(event_names:, start_date:, end_date:)
|
||||
count_unique_events(event_names: event_names, start_date: start_date, end_date: end_date)
|
||||
count_unique_events(event_names: event_names, property_name: nil, start_date: start_date, end_date: end_date)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def track(values, event_name, time: Time.zone.now)
|
||||
def track(values, event_name, property_name:, time: Time.zone.now)
|
||||
event = event_for(event_name)
|
||||
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(UnknownEvent.new("Unknown event #{event_name}")) unless event.present?
|
||||
|
||||
return if event.blank?
|
||||
return unless Feature.enabled?(:redis_hll_tracking, type: :ops)
|
||||
|
||||
Gitlab::Redis::HLL.add(key: redis_key(event, time), value: values, expiry: KEY_EXPIRY_LENGTH)
|
||||
Gitlab::Redis::HLL.add(key: redis_key(event_with_property_name(event, property_name), time), value: values, expiry: KEY_EXPIRY_LENGTH)
|
||||
|
||||
rescue StandardError => e
|
||||
# Ignore any exceptions unless is dev or test env
|
||||
|
|
@ -72,8 +74,8 @@ module Gitlab
|
|||
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
|
||||
end
|
||||
|
||||
def count_unique_events(event_names:, start_date:, end_date:)
|
||||
events = events_for(Array(event_names).map(&:to_s))
|
||||
def count_unique_events(event_names:, start_date:, end_date:, property_name:)
|
||||
events = events_with_property_names(event_names, property_name)
|
||||
|
||||
keys = keys_for_aggregation(events: events, start_date: start_date, end_date: end_date)
|
||||
|
||||
|
|
@ -82,6 +84,19 @@ module Gitlab
|
|||
redis_usage_data { Gitlab::Redis::HLL.count(keys: keys) }
|
||||
end
|
||||
|
||||
def events_with_property_names(event_names, property_name)
|
||||
event_names = Array(event_names).map(&:to_s)
|
||||
known_events.filter_map do |event|
|
||||
next unless event_names.include?(event[:name])
|
||||
|
||||
property_name ? event_with_property_name(event, property_name) : event
|
||||
end
|
||||
end
|
||||
|
||||
def event_with_property_name(event, property_name)
|
||||
event.merge(property_name: property_name)
|
||||
end
|
||||
|
||||
def load_events
|
||||
events = Gitlab::Usage::MetricDefinition.all.map do |d|
|
||||
next unless d.available?
|
||||
|
|
@ -102,21 +117,30 @@ module Gitlab
|
|||
known_events.find { |event| event[:name] == event_name.to_s }
|
||||
end
|
||||
|
||||
def events_for(event_names)
|
||||
known_events.select { |event| event_names.include?(event[:name]) }
|
||||
end
|
||||
|
||||
def redis_key(event, time)
|
||||
key = redis_key_base(event[:name])
|
||||
key = redis_key_base(event)
|
||||
|
||||
year_week = time.strftime('%G-%V')
|
||||
"{#{REDIS_SLOT}}_#{key}-#{year_week}"
|
||||
end
|
||||
|
||||
def redis_key_base(event_name)
|
||||
def redis_key_base(event)
|
||||
event_name = event[:name]
|
||||
|
||||
raise UnknownEvent, "Unknown event #{event_name}" unless known_events_names.include?(event_name.to_s)
|
||||
|
||||
key_overrides.fetch(event_name, event_name)
|
||||
property_name = event[:property_name]
|
||||
key = event_name
|
||||
if Feature.enabled?(:redis_hll_property_name_tracking, type: :wip) && property_name
|
||||
key = "#{key}-#{formatted_property_name(property_name)}"
|
||||
end
|
||||
|
||||
key_overrides.fetch(key, key)
|
||||
end
|
||||
|
||||
def formatted_property_name(property_name)
|
||||
# simplify to format from EventDefinitions.unique_properties
|
||||
property_name.to_s.split('.').first.to_sym
|
||||
end
|
||||
|
||||
def key_overrides
|
||||
|
|
|
|||
|
|
@ -1 +1,127 @@
|
|||
{}
|
||||
# This file lists all of the internal events that need to be saved with their legacy HLL Redis keys
|
||||
#
|
||||
# This file has been generated using the script included in
|
||||
# the description of https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137890
|
||||
#
|
||||
# It is only safe to regenerate it using the same script if the
|
||||
# :redis_hll_property_name_tracking feature flag is disabled on prod environment.
|
||||
---
|
||||
agent_users_using_ci_tunnel-user: agent_users_using_ci_tunnel
|
||||
ci_template_included-project: ci_template_included
|
||||
exclude_anonymised_users-user: exclude_anonymised_users
|
||||
g_compliance_dashboard-user: g_compliance_dashboard
|
||||
g_edit_by_sfe-user: g_edit_by_sfe
|
||||
g_edit_by_snippet_ide-user: g_edit_by_snippet_ide
|
||||
g_edit_by_web_ide-user: g_edit_by_web_ide
|
||||
g_project_management_epic_blocked_added-user: g_project_management_epic_blocked_added
|
||||
g_project_management_epic_blocked_removed-user: g_project_management_epic_blocked_removed
|
||||
g_project_management_epic_blocking_added-user: g_project_management_epic_blocking_added
|
||||
g_project_management_epic_blocking_removed-user: g_project_management_epic_blocking_removed
|
||||
g_project_management_epic_closed-user: g_project_management_epic_closed
|
||||
g_project_management_epic_created-user: g_project_management_epic_created
|
||||
g_project_management_epic_cross_referenced-user: g_project_management_epic_cross_referenced
|
||||
g_project_management_epic_destroyed-user: g_project_management_epic_destroyed
|
||||
g_project_management_epic_issue_added-user: g_project_management_epic_issue_added
|
||||
g_project_management_epic_issue_moved_from_project-user: g_project_management_epic_issue_moved_from_project
|
||||
g_project_management_epic_issue_removed-user: g_project_management_epic_issue_removed
|
||||
g_project_management_epic_related_added-user: g_project_management_epic_related_added
|
||||
g_project_management_epic_related_removed-user: g_project_management_epic_related_removed
|
||||
g_project_management_epic_reopened-user: g_project_management_epic_reopened
|
||||
g_project_management_epic_users_changing_labels-user: g_project_management_epic_users_changing_labels
|
||||
g_project_management_issue_added_to_epic-user: g_project_management_issue_added_to_epic
|
||||
g_project_management_issue_assignee_changed-user: g_project_management_issue_assignee_changed
|
||||
g_project_management_issue_changed_epic-user: g_project_management_issue_changed_epic
|
||||
g_project_management_issue_cloned-user: g_project_management_issue_cloned
|
||||
g_project_management_issue_closed-user: g_project_management_issue_closed
|
||||
g_project_management_issue_comment_added-user: g_project_management_issue_comment_added
|
||||
g_project_management_issue_comment_edited-user: g_project_management_issue_comment_edited
|
||||
g_project_management_issue_comment_removed-user: g_project_management_issue_comment_removed
|
||||
g_project_management_issue_created-user: g_project_management_issue_created
|
||||
g_project_management_issue_cross_referenced-user: g_project_management_issue_cross_referenced
|
||||
g_project_management_issue_description_changed-user: g_project_management_issue_description_changed
|
||||
g_project_management_issue_design_comments_removed-user: g_project_management_issue_design_comments_removed
|
||||
g_project_management_issue_designs_added-user: g_project_management_issue_designs_added
|
||||
g_project_management_issue_designs_modified-user: g_project_management_issue_designs_modified
|
||||
g_project_management_issue_designs_removed-user: g_project_management_issue_designs_removed
|
||||
g_project_management_issue_due_date_changed-user: g_project_management_issue_due_date_changed
|
||||
g_project_management_issue_health_status_changed-user: g_project_management_issue_health_status_changed
|
||||
g_project_management_issue_iteration_changed-user: g_project_management_issue_iteration_changed
|
||||
g_project_management_issue_label_changed-user: g_project_management_issue_label_changed
|
||||
g_project_management_issue_locked-user: g_project_management_issue_locked
|
||||
g_project_management_issue_made_confidential-user: g_project_management_issue_made_confidential
|
||||
g_project_management_issue_made_visible-user: g_project_management_issue_made_visible
|
||||
g_project_management_issue_marked_as_duplicate-user: g_project_management_issue_marked_as_duplicate
|
||||
g_project_management_issue_milestone_changed-user: g_project_management_issue_milestone_changed
|
||||
g_project_management_issue_moved-user: g_project_management_issue_moved
|
||||
g_project_management_issue_promoted_to_epic-user: g_project_management_issue_promoted_to_epic
|
||||
g_project_management_issue_related-user: g_project_management_issue_related
|
||||
g_project_management_issue_removed_from_epic-user: g_project_management_issue_removed_from_epic
|
||||
g_project_management_issue_reopened-user: g_project_management_issue_reopened
|
||||
g_project_management_issue_time_estimate_changed-user: g_project_management_issue_time_estimate_changed
|
||||
g_project_management_issue_time_spent_changed-user: g_project_management_issue_time_spent_changed
|
||||
g_project_management_issue_title_changed-user: g_project_management_issue_title_changed
|
||||
g_project_management_issue_unlocked-user: g_project_management_issue_unlocked
|
||||
g_project_management_issue_unrelated-user: g_project_management_issue_unrelated
|
||||
g_project_management_issue_weight_changed-user: g_project_management_issue_weight_changed
|
||||
g_project_management_users_awarding_epic_emoji-user: g_project_management_users_awarding_epic_emoji
|
||||
g_project_management_users_creating_epic_notes-user: g_project_management_users_creating_epic_notes
|
||||
g_project_management_users_destroying_epic_notes-user: g_project_management_users_destroying_epic_notes
|
||||
g_project_management_users_epic_issue_added_from_epic-user: g_project_management_users_epic_issue_added_from_epic
|
||||
g_project_management_users_removing_epic_emoji-user: g_project_management_users_removing_epic_emoji
|
||||
g_project_management_users_setting_epic_confidential-user: g_project_management_users_setting_epic_confidential
|
||||
g_project_management_users_setting_epic_due_date_as_fixed-user: g_project_management_users_setting_epic_due_date_as_fixed
|
||||
g_project_management_users_setting_epic_due_date_as_inherited-user: g_project_management_users_setting_epic_due_date_as_inherited
|
||||
g_project_management_users_setting_epic_start_date_as_fixed-user: g_project_management_users_setting_epic_start_date_as_fixed
|
||||
g_project_management_users_setting_epic_start_date_as_inherited-user: g_project_management_users_setting_epic_start_date_as_inherited
|
||||
g_project_management_users_setting_epic_visible-user: g_project_management_users_setting_epic_visible
|
||||
g_project_management_users_updating_epic_descriptions-user: g_project_management_users_updating_epic_descriptions
|
||||
g_project_management_users_updating_epic_notes-user: g_project_management_users_updating_epic_notes
|
||||
g_project_management_users_updating_epic_parent-user: g_project_management_users_updating_epic_parent
|
||||
g_project_management_users_updating_epic_titles-user: g_project_management_users_updating_epic_titles
|
||||
g_project_management_users_updating_fixed_epic_due_date-user: g_project_management_users_updating_fixed_epic_due_date
|
||||
g_project_management_users_updating_fixed_epic_start_date-user: g_project_management_users_updating_fixed_epic_start_date
|
||||
geo_secondary_git_op_action-user: geo_secondary_git_op_action
|
||||
i_analytics_dev_ops_adoption-user: i_analytics_dev_ops_adoption
|
||||
i_analytics_dev_ops_score-user: i_analytics_dev_ops_score
|
||||
i_code_review_saved_replies_create-user: i_code_review_saved_replies_create
|
||||
i_code_review_saved_replies_use-user: i_code_review_saved_replies_use
|
||||
i_code_review_saved_replies_use_in_mr-user: i_code_review_saved_replies_use_in_mr
|
||||
i_code_review_saved_replies_use_in_other-user: i_code_review_saved_replies_use_in_other
|
||||
i_code_review_user_create_mr-user: i_code_review_user_create_mr
|
||||
i_quickactions_remove_email_multiple-user: i_quickactions_remove_email_multiple
|
||||
i_quickactions_remove_email_single-user: i_quickactions_remove_email_single
|
||||
insights_chart_item_clicked-user: insights_chart_item_clicked
|
||||
insights_issue_chart_item_clicked-user: insights_issue_chart_item_clicked
|
||||
k8s_api_proxy_requests_unique_users_via_ci_access-user: k8s_api_proxy_requests_unique_users_via_ci_access
|
||||
k8s_api_proxy_requests_unique_users_via_pat_access-user: k8s_api_proxy_requests_unique_users_via_pat_access
|
||||
k8s_api_proxy_requests_unique_users_via_user_access-user: k8s_api_proxy_requests_unique_users_via_user_access
|
||||
p_analytics_ci_cd_deployment_frequency-user: p_analytics_ci_cd_deployment_frequency
|
||||
p_analytics_ci_cd_lead_time-user: p_analytics_ci_cd_lead_time
|
||||
p_analytics_ci_cd_pipelines-user: p_analytics_ci_cd_pipelines
|
||||
project_management_users_checking_epic_task-user: project_management_users_checking_epic_task
|
||||
project_management_users_unchecking_epic_task-user: project_management_users_unchecking_epic_task
|
||||
unique_users_visiting_ci_catalog-user: unique_users_visiting_ci_catalog
|
||||
user_created_custom_dashboard-user: user_created_custom_dashboard
|
||||
user_created_custom_visualization-user: user_created_custom_visualization
|
||||
user_edited_cluster_configuration-user: user_edited_cluster_configuration
|
||||
user_edited_custom_dashboard-user: user_edited_custom_dashboard
|
||||
user_viewed_cluster_configuration-user: user_viewed_cluster_configuration
|
||||
user_viewed_custom_dashboard-user: user_viewed_custom_dashboard
|
||||
user_viewed_dashboard_designer-user: user_viewed_dashboard_designer
|
||||
user_viewed_dashboard_list-user: user_viewed_dashboard_list
|
||||
user_viewed_instrumentation_directions-user: user_viewed_instrumentation_directions
|
||||
user_viewed_visualization_designer-user: user_viewed_visualization_designer
|
||||
user_visited_dashboard-user: user_visited_dashboard
|
||||
value_streams_dashboard_change_failure_rate_link_clicked-user: value_streams_dashboard_change_failure_rate_link_clicked
|
||||
value_streams_dashboard_cycle_time_link_clicked-user: value_streams_dashboard_cycle_time_link_clicked
|
||||
value_streams_dashboard_deployment_frequency_link_clicked-user: value_streams_dashboard_deployment_frequency_link_clicked
|
||||
value_streams_dashboard_deploys_link_clicked-user: value_streams_dashboard_deploys_link_clicked
|
||||
value_streams_dashboard_issues_completed_link_clicked-user: value_streams_dashboard_issues_completed_link_clicked
|
||||
value_streams_dashboard_issues_link_clicked-user: value_streams_dashboard_issues_link_clicked
|
||||
value_streams_dashboard_lead_time_for_changes_link_clicked-user: value_streams_dashboard_lead_time_for_changes_link_clicked
|
||||
value_streams_dashboard_lead_time_link_clicked-user: value_streams_dashboard_lead_time_link_clicked
|
||||
value_streams_dashboard_merge_request_throughput_link_clicked-user: value_streams_dashboard_merge_request_throughput_link_clicked
|
||||
value_streams_dashboard_metric_link_clicked-user: value_streams_dashboard_metric_link_clicked
|
||||
value_streams_dashboard_time_to_restore_service_link_clicked-user: value_streams_dashboard_time_to_restore_service_link_clicked
|
||||
value_streams_dashboard_vulnerability_critical_link_clicked-user: value_streams_dashboard_vulnerability_critical_link_clicked
|
||||
value_streams_dashboard_vulnerability_high_link_clicked-user: value_streams_dashboard_vulnerability_high_link_clicked
|
||||
|
|
|
|||
|
|
@ -1585,11 +1585,6 @@ msgstr ""
|
|||
msgid "0 B"
|
||||
msgstr ""
|
||||
|
||||
msgid "1 Code Quality finding"
|
||||
msgid_plural "%d Code Quality findings"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "1 Day"
|
||||
msgid_plural "%d Days"
|
||||
msgstr[0] ""
|
||||
|
|
@ -5517,9 +5512,6 @@ msgstr ""
|
|||
msgid "An unexpected error occurred while communicating with the Web Terminal."
|
||||
msgstr ""
|
||||
|
||||
msgid "An unexpected error occurred while loading the code quality diff."
|
||||
msgstr ""
|
||||
|
||||
msgid "An unexpected error occurred while starting the Web Terminal."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -23063,6 +23055,18 @@ msgstr ""
|
|||
msgid "Google Play service account key."
|
||||
msgstr ""
|
||||
|
||||
msgid "GoogleArtifactRegistry|An error occurred while fetching the artifacts."
|
||||
msgstr ""
|
||||
|
||||
msgid "GoogleArtifactRegistry|Open in Google Cloud"
|
||||
msgstr ""
|
||||
|
||||
msgid "GoogleArtifactRegistry|Project ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "GoogleArtifactRegistry|Repository name"
|
||||
msgstr ""
|
||||
|
||||
msgid "GoogleCloud|Cancel"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ RSpec.describe 'Groups > Members > Sort members', :js, feature_category: :groups
|
|||
|
||||
def expect_sort_by(text, sort_direction)
|
||||
within_testid('members-sort-dropdown') do
|
||||
expect(page).to have_css('button[aria-haspopup="menu"]', text: text)
|
||||
expect(page).to have_css('button[aria-haspopup="listbox"]', text: text)
|
||||
expect(page).to have_button("Sort direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ RSpec.describe 'Projects > Members > Sorting', :js, feature_category: :groups_an
|
|||
|
||||
def expect_sort_by(text, sort_direction)
|
||||
within('[data-testid="members-sort-dropdown"]') do
|
||||
expect(page).to have_css('button[aria-haspopup="menu"]', text: text)
|
||||
expect(page).to have_css('button[aria-haspopup="listbox"]', text: text)
|
||||
expect(page).to have_button("Sort direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Groups::AcceptingProjectTransfersFinder do
|
||||
RSpec.describe Groups::AcceptingProjectTransfersFinder, feature_category: :groups_and_projects do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group_where_direct_owner) { create(:group) }
|
||||
let_it_be(:subgroup_of_group_where_direct_owner) { create(:group, parent: group_where_direct_owner) }
|
||||
|
|
|
|||
|
|
@ -195,10 +195,6 @@ describe('diffs/components/app', () => {
|
|||
describe('codequality diff', () => {
|
||||
it('does not fetch code quality data on FOSS', () => {
|
||||
createComponent();
|
||||
jest.spyOn(wrapper.vm, 'fetchCodequality');
|
||||
wrapper.vm.fetchData(false);
|
||||
|
||||
expect(wrapper.vm.fetchCodequality).not.toHaveBeenCalled();
|
||||
expect(codeQualityAndSastQueryHandlerSuccess).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
import { GlIcon } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import DiffInlineFindingsItem from '~/diffs/components/diff_inline_findings_item.vue';
|
||||
import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/ci/reports/codequality_report/constants';
|
||||
import { multipleFindingsArrCodeQualityScale } from '../mock_data/inline_findings';
|
||||
|
||||
let wrapper;
|
||||
|
||||
const [codeQualityFinding] = multipleFindingsArrCodeQualityScale;
|
||||
const findIcon = () => wrapper.findComponent(GlIcon);
|
||||
const findDescriptionPlainText = () => wrapper.findByTestId('description-plain-text');
|
||||
|
||||
describe('DiffCodeQuality', () => {
|
||||
const createWrapper = () => {
|
||||
return shallowMountExtended(DiffInlineFindingsItem, {
|
||||
propsData: {
|
||||
finding: codeQualityFinding,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
it('shows icon for given degradation', () => {
|
||||
wrapper = createWrapper();
|
||||
expect(findIcon().exists()).toBe(true);
|
||||
|
||||
expect(findIcon().attributes()).toMatchObject({
|
||||
class: `inline-findings-severity-icon ${SEVERITY_CLASSES[codeQualityFinding.severity]}`,
|
||||
name: SEVERITY_ICONS[codeQualityFinding.severity],
|
||||
size: '12',
|
||||
});
|
||||
});
|
||||
|
||||
it('should render severity + description in plain text', () => {
|
||||
wrapper = createWrapper();
|
||||
expect(findDescriptionPlainText().text()).toContain(codeQualityFinding.severity);
|
||||
expect(findDescriptionPlainText().text()).toContain(codeQualityFinding.description);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import DiffInlineFindings from '~/diffs/components/diff_inline_findings.vue';
|
||||
import DiffInlineFindingsItem from '~/diffs/components/diff_inline_findings_item.vue';
|
||||
import { NEW_CODE_QUALITY_FINDINGS } from '~/diffs/i18n';
|
||||
import { multipleFindingsArrCodeQualityScale } from '../mock_data/inline_findings';
|
||||
|
||||
let wrapper;
|
||||
const heading = () => wrapper.findByTestId('diff-inline-findings-heading');
|
||||
const diffInlineFindingsItems = () => wrapper.findAllComponents(DiffInlineFindingsItem);
|
||||
|
||||
describe('DiffInlineFindings', () => {
|
||||
const createWrapper = () => {
|
||||
return shallowMountExtended(DiffInlineFindings, {
|
||||
propsData: {
|
||||
title: NEW_CODE_QUALITY_FINDINGS,
|
||||
findings: multipleFindingsArrCodeQualityScale,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
it('renders the title correctly', () => {
|
||||
wrapper = createWrapper();
|
||||
expect(heading().text()).toBe(NEW_CODE_QUALITY_FINDINGS);
|
||||
});
|
||||
|
||||
it('renders the correct number of DiffInlineFindingsItem components with correct props', () => {
|
||||
wrapper = createWrapper();
|
||||
expect(diffInlineFindingsItems()).toHaveLength(multipleFindingsArrCodeQualityScale.length);
|
||||
expect(diffInlineFindingsItems().wrappers[0].props('finding')).toEqual(
|
||||
wrapper.props('findings')[0],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import DiffLine from '~/diffs/components/diff_line.vue';
|
||||
import InlineFindings from '~/diffs/components/inline_findings.vue';
|
||||
|
||||
const EXAMPLE_LINE_NUMBER = 3;
|
||||
const EXAMPLE_DESCRIPTION = 'example description';
|
||||
const EXAMPLE_SEVERITY = 'example severity';
|
||||
|
||||
const left = {
|
||||
line: {
|
||||
left: {
|
||||
codequality: [
|
||||
{
|
||||
line: EXAMPLE_LINE_NUMBER,
|
||||
description: EXAMPLE_DESCRIPTION,
|
||||
severity: EXAMPLE_SEVERITY,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const right = {
|
||||
line: {
|
||||
right: {
|
||||
codequality: [
|
||||
{
|
||||
line: EXAMPLE_LINE_NUMBER,
|
||||
description: EXAMPLE_DESCRIPTION,
|
||||
severity: EXAMPLE_SEVERITY,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockData = [right, left];
|
||||
|
||||
describe('DiffLine', () => {
|
||||
const createWrapper = (propsData) => {
|
||||
return shallowMount(DiffLine, { propsData });
|
||||
};
|
||||
|
||||
it('should emit event when hideInlineFindings is called', () => {
|
||||
const wrapper = createWrapper(right);
|
||||
|
||||
wrapper.findComponent(InlineFindings).vm.$emit('hideInlineFindings');
|
||||
expect(wrapper.emitted()).toEqual({
|
||||
hideInlineFindings: [[EXAMPLE_LINE_NUMBER]],
|
||||
});
|
||||
});
|
||||
|
||||
mockData.forEach((element) => {
|
||||
it('should set correct props for InlineFindings', () => {
|
||||
const wrapper = createWrapper(element);
|
||||
expect(wrapper.findComponent(InlineFindings).props('codeQuality')).toEqual([
|
||||
{
|
||||
line: EXAMPLE_LINE_NUMBER,
|
||||
description: EXAMPLE_DESCRIPTION,
|
||||
severity: EXAMPLE_SEVERITY,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import Vue from 'vue';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import Vuex from 'vuex';
|
||||
import { throttle } from 'lodash';
|
||||
import DiffView from '~/diffs/components/diff_view.vue';
|
||||
import DiffLine from '~/diffs/components/diff_line.vue';
|
||||
import { diffCodeQuality } from '../mock_data/inline_findings';
|
||||
|
||||
jest.mock('lodash/throttle', () => jest.fn((fn) => fn));
|
||||
const lodash = jest.requireActual('lodash');
|
||||
|
|
@ -19,7 +17,7 @@ describe('DiffView', () => {
|
|||
const setSelectedCommentPosition = jest.fn();
|
||||
const getDiffRow = (wrapper) => wrapper.findComponent(DiffRow).vm;
|
||||
|
||||
const createWrapper = ({ props, flag = false } = {}) => {
|
||||
const createWrapper = ({ props } = {}) => {
|
||||
Vue.use(Vuex);
|
||||
|
||||
const batchComments = {
|
||||
|
|
@ -51,21 +49,10 @@ describe('DiffView', () => {
|
|||
diffFile: { file_hash: '123' },
|
||||
diffLines: [],
|
||||
...props,
|
||||
provide: {
|
||||
glFeatures: {
|
||||
sastReportsInInlineDiff: flag,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const provide = {
|
||||
glFeatures: {
|
||||
sastReportsInInlineDiff: flag,
|
||||
},
|
||||
};
|
||||
|
||||
const stubs = { DiffExpansionCell, DiffRow, DiffCommentCell, DraftNote };
|
||||
return shallowMount(DiffView, { propsData, provide, store, stubs });
|
||||
return shallowMount(DiffView, { propsData, store, stubs });
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -76,32 +63,6 @@ describe('DiffView', () => {
|
|||
throttle.mockReset();
|
||||
});
|
||||
|
||||
it('does not render a diff-line component when there is no finding', () => {
|
||||
const wrapper = createWrapper();
|
||||
expect(wrapper.findComponent(DiffLine).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does render a diff-line component with the correct props when there is a finding', async () => {
|
||||
const wrapper = createWrapper({ props: diffCodeQuality });
|
||||
wrapper.findComponent(DiffRow).vm.$emit('toggleCodeQualityFindings', 2);
|
||||
await nextTick();
|
||||
expect(wrapper.findComponent(DiffLine).props('line')).toBe(diffCodeQuality.diffLines[2]);
|
||||
});
|
||||
|
||||
it('does not render a diff-line component when there is a finding and sastReportsInInlineDiff flag is true', async () => {
|
||||
const wrapper = createWrapper({ props: diffCodeQuality, flag: true });
|
||||
wrapper.findComponent(DiffRow).vm.$emit('toggleCodeQualityFindings', 2);
|
||||
await nextTick();
|
||||
expect(wrapper.findComponent(DiffLine).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does render a diff-line component when there is a finding and sastReportsInInlineDiff flag is false', async () => {
|
||||
const wrapper = createWrapper({ props: diffCodeQuality });
|
||||
wrapper.findComponent(DiffRow).vm.$emit('toggleCodeQualityFindings', 2);
|
||||
await nextTick();
|
||||
expect(wrapper.findComponent(DiffLine).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it.each`
|
||||
type | side | container | sides | total
|
||||
${'parallel'} | ${'left'} | ${'.old'} | ${{ left: { lineDrafts: [], renderDiscussion: true }, right: { lineDrafts: [], renderDiscussion: true } }} | ${2}
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import InlineFindings from '~/diffs/components/inline_findings.vue';
|
||||
import DiffInlineFindings from '~/diffs/components/diff_inline_findings.vue';
|
||||
import { NEW_CODE_QUALITY_FINDINGS } from '~/diffs/i18n';
|
||||
import { threeCodeQualityFindings } from '../mock_data/inline_findings';
|
||||
|
||||
let wrapper;
|
||||
|
||||
const diffInlineFindings = () => wrapper.findComponent(DiffInlineFindings);
|
||||
|
||||
describe('InlineFindings', () => {
|
||||
const createWrapper = () => {
|
||||
return mountExtended(InlineFindings, {
|
||||
propsData: {
|
||||
codeQuality: threeCodeQualityFindings,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
it('hides details and throws hideInlineFindings event on close click', async () => {
|
||||
wrapper = createWrapper();
|
||||
expect(wrapper.findByTestId('inline-findings').exists()).toBe(true);
|
||||
|
||||
await wrapper.findByTestId('inline-findings-close').trigger('click');
|
||||
expect(wrapper.emitted('hideInlineFindings')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders diff inline findings component with correct props for codequality array', () => {
|
||||
wrapper = createWrapper();
|
||||
expect(diffInlineFindings().props('title')).toBe(NEW_CODE_QUALITY_FINDINGS);
|
||||
expect(diffInlineFindings().props('findings')).toBe(threeCodeQualityFindings);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import { pickDirection } from '~/diffs/utils/diff_line';
|
||||
|
||||
describe('diff_line utilities', () => {
|
||||
describe('pickDirection', () => {
|
||||
const left = {
|
||||
line_code: 'left',
|
||||
};
|
||||
const right = {
|
||||
line_code: 'right',
|
||||
};
|
||||
const defaultLine = {
|
||||
left,
|
||||
right,
|
||||
};
|
||||
|
||||
it.each`
|
||||
code | pick | line | pickDescription
|
||||
${'left'} | ${left} | ${defaultLine} | ${'the left line'}
|
||||
${'right'} | ${right} | ${defaultLine} | ${'the right line'}
|
||||
${'junk'} | ${left} | ${defaultLine} | ${'the default: the left line'}
|
||||
${'junk'} | ${right} | ${{ right }} | ${"the right line if there's no left line to default to"}
|
||||
${'right'} | ${left} | ${{ left }} | ${"the left line when there isn't a right line to match"}
|
||||
`(
|
||||
'when provided a line and a line code `$code`, picks $pickDescription',
|
||||
({ code, line, pick }) => {
|
||||
expect(pickDirection({ line, code })).toBe(pick);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { GlSorting, GlSortingItem } from '@gitlab/ui';
|
||||
import { GlSorting } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import Vuex from 'vuex';
|
||||
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||
import * as urlUtilities from '~/lib/utils/url_utility';
|
||||
import SortDropdown from '~/members/components/filter_sort/sort_dropdown.vue';
|
||||
import { MEMBER_TYPES } from '~/members/constants';
|
||||
import { MEMBER_TYPES, FIELD_KEY_MAX_ROLE } from '~/members/constants';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
|
|
@ -47,59 +47,46 @@ describe('SortDropdown', () => {
|
|||
const findSortingComponent = () => wrapper.findComponent(GlSorting);
|
||||
const findSortDirectionToggle = () =>
|
||||
findSortingComponent().find('button[title^="Sort direction"]');
|
||||
const findDropdownToggle = () => wrapper.find('button[aria-haspopup="menu"]');
|
||||
const findDropdownItemByText = (text) =>
|
||||
wrapper
|
||||
.findAllComponents(GlSortingItem)
|
||||
.wrappers.find((dropdownItemWrapper) => dropdownItemWrapper.text() === text);
|
||||
const findDropdownToggle = () => wrapper.find('button[aria-haspopup="listbox"]');
|
||||
|
||||
beforeEach(() => {
|
||||
setWindowLocation(URL_HOST);
|
||||
});
|
||||
|
||||
describe('dropdown options', () => {
|
||||
it('adds dropdown items for all the sortable fields', () => {
|
||||
it('sets sort options', () => {
|
||||
const URL_FILTER_PARAMS = '?two_factor=enabled&search=foobar';
|
||||
const EXPECTED_BASE_URL = `${URL_HOST}${URL_FILTER_PARAMS}&sort=`;
|
||||
|
||||
setWindowLocation(URL_FILTER_PARAMS);
|
||||
|
||||
const expectedDropdownItems = [
|
||||
const expectedSortOptions = [
|
||||
{
|
||||
label: 'Account',
|
||||
url: `${EXPECTED_BASE_URL}name_asc`,
|
||||
text: 'Account',
|
||||
value: 'account',
|
||||
},
|
||||
{
|
||||
label: 'Access granted',
|
||||
url: `${EXPECTED_BASE_URL}last_joined`,
|
||||
text: 'Access granted',
|
||||
value: 'granted',
|
||||
},
|
||||
{
|
||||
label: 'Max role',
|
||||
url: `${EXPECTED_BASE_URL}access_level_asc`,
|
||||
text: 'Max role',
|
||||
value: 'maxRole',
|
||||
},
|
||||
{
|
||||
label: 'Last sign-in',
|
||||
url: `${EXPECTED_BASE_URL}recent_sign_in`,
|
||||
text: 'Last sign-in',
|
||||
value: 'lastSignIn',
|
||||
},
|
||||
];
|
||||
|
||||
createComponent();
|
||||
|
||||
expectedDropdownItems.forEach((expectedDropdownItem) => {
|
||||
const dropdownItem = findDropdownItemByText(expectedDropdownItem.label);
|
||||
|
||||
expect(dropdownItem).not.toBe(null);
|
||||
expect(dropdownItem.find('a').attributes('href')).toBe(expectedDropdownItem.url);
|
||||
expect(findSortingComponent().props()).toMatchObject({
|
||||
text: expectedSortOptions[0].text,
|
||||
isAscending: true,
|
||||
sortBy: expectedSortOptions[0].value,
|
||||
sortOptions: expectedSortOptions,
|
||||
});
|
||||
});
|
||||
|
||||
it('checks selected sort option', () => {
|
||||
setWindowLocation('?sort=access_level_asc');
|
||||
|
||||
createComponent();
|
||||
|
||||
expect(findDropdownItemByText('Max role').vm.$attrs.active).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dropdown toggle', () => {
|
||||
|
|
@ -117,6 +104,20 @@ describe('SortDropdown', () => {
|
|||
|
||||
expect(findDropdownToggle().text()).toBe('Max role');
|
||||
});
|
||||
|
||||
describe('select new sort field', () => {
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(urlUtilities, 'visitUrl').mockImplementation();
|
||||
createComponent();
|
||||
|
||||
findSortingComponent().vm.$emit('sortByChange', FIELD_KEY_MAX_ROLE);
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
it('sorts by new field', () => {
|
||||
expect(urlUtilities.visitUrl).toHaveBeenCalledWith(`${URL_HOST}?sort=access_level_asc`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sort direction toggle', () => {
|
||||
|
|
|
|||
|
|
@ -74,14 +74,14 @@ RSpec.describe Gitlab::Instrumentation::RedisClientMiddleware, :request_store, f
|
|||
context 'when encountering exceptions' do
|
||||
before do
|
||||
allow(redis_client.instance_variable_get(:@raw_connection)).to receive(:call).and_raise(
|
||||
RedisClient::ConnectionError, 'Connection was closed or lost')
|
||||
RedisClient::Error)
|
||||
end
|
||||
|
||||
it 'counts exception' do
|
||||
expect(instrumentation_class).to receive(:instance_count_exception)
|
||||
.with(instance_of(RedisClient::ConnectionError)).and_call_original
|
||||
.with(instance_of(RedisClient::Error)).and_call_original
|
||||
expect(instrumentation_class).to receive(:log_exception)
|
||||
.with(instance_of(RedisClient::ConnectionError)).and_call_original
|
||||
.with(instance_of(RedisClient::Error)).and_call_original
|
||||
expect(instrumentation_class).to receive(:instance_count_request).and_call_original
|
||||
|
||||
expect do
|
||||
|
|
|
|||
|
|
@ -37,20 +37,27 @@ RSpec.describe Gitlab::Instrumentation::RedisHelper, :request_store, feature_cat
|
|||
subject(:minimal_test_class_instance) { MinimalTestClass.new }
|
||||
|
||||
describe '.instrument_call' do
|
||||
let(:pipelined) { false }
|
||||
let(:command) { [[:set, 'foo', 'bar']] }
|
||||
|
||||
subject(:instrumented_command) { minimal_test_class_instance.check_command(command, pipelined) }
|
||||
|
||||
it 'instruments request count' do
|
||||
expect(Gitlab::Instrumentation::Redis::Cache).to receive(:instance_count_request).with(1)
|
||||
expect(Gitlab::Instrumentation::Redis::Cache).not_to receive(:instance_count_pipelined_request)
|
||||
|
||||
minimal_test_class_instance.check_command([[:set, 'foo', 'bar']], false)
|
||||
instrumented_command
|
||||
end
|
||||
|
||||
it 'performs cluster validation' do
|
||||
expect(Gitlab::Instrumentation::Redis::Cache).to receive(:redis_cluster_validate!).once
|
||||
|
||||
minimal_test_class_instance.check_command([[:set, 'foo', 'bar']], false)
|
||||
instrumented_command
|
||||
end
|
||||
|
||||
context 'when command is not valid for Redis Cluster' do
|
||||
let(:command) { [[:mget, 'foo', 'bar']] }
|
||||
|
||||
before do
|
||||
allow(Gitlab::Instrumentation::Redis::Cache).to receive(:redis_cluster_validate!).and_return(false)
|
||||
end
|
||||
|
|
@ -58,7 +65,7 @@ RSpec.describe Gitlab::Instrumentation::RedisHelper, :request_store, feature_cat
|
|||
it 'reports cross slot request' do
|
||||
expect(Gitlab::Instrumentation::Redis::Cache).to receive(:increment_cross_slot_request_count).once
|
||||
|
||||
minimal_test_class_instance.check_command([[:mget, 'foo', 'bar']], false)
|
||||
instrumented_command
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -71,21 +78,52 @@ RSpec.describe Gitlab::Instrumentation::RedisHelper, :request_store, feature_cat
|
|||
end
|
||||
|
||||
it 'ensures duration is tracked' do
|
||||
commands = [[:set, 'foo', 'bar']]
|
||||
allow(Gitlab::Instrumentation::Redis::Cache).to receive(:instance_observe_duration).once
|
||||
allow(Gitlab::Instrumentation::Redis::Cache).to receive(:increment_request_count).with(1).once
|
||||
allow(Gitlab::Instrumentation::Redis::Cache).to receive(:add_duration).once
|
||||
allow(Gitlab::Instrumentation::Redis::Cache).to receive(:add_call_details).with(anything, commands).once
|
||||
allow(Gitlab::Instrumentation::Redis::Cache).to receive(:add_call_details).with(anything, command).once
|
||||
|
||||
expect { minimal_test_class_instance.check_command(commands, false) }.to raise_error(StandardError)
|
||||
expect { instrumented_command }.to raise_error(StandardError)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a RedisClient::ConnectionError is raised' do
|
||||
before do
|
||||
allow(Gitlab::Instrumentation::Redis::Cache).to receive(:instance_count_request)
|
||||
.and_raise(RedisClient::ConnectionError)
|
||||
end
|
||||
|
||||
it 'silences connection errors raised during the first attempt' do
|
||||
expect(Gitlab::Instrumentation::Redis::Cache).not_to receive(:log_exception).with(RedisClient::ConnectionError)
|
||||
|
||||
expect { instrumented_command }.to raise_error(StandardError)
|
||||
|
||||
expect(Thread.current[:redis_client_error_count]).to eq(1)
|
||||
end
|
||||
|
||||
context 'when error is raised on the second attempt' do
|
||||
before do
|
||||
Thread.current[:redis_client_error_count] = 1
|
||||
end
|
||||
|
||||
it 'instruments errors on second attempt' do
|
||||
expect(Gitlab::Instrumentation::Redis::Cache).to receive(:log_exception).with(RedisClient::ConnectionError)
|
||||
|
||||
expect { instrumented_command }.to raise_error(StandardError)
|
||||
|
||||
expect(Thread.current[:redis_client_error_count]).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pipelined' do
|
||||
let(:command) { [[:get, '{user1}:bar'], [:get, '{user1}:foo']] }
|
||||
let(:pipelined) { true }
|
||||
|
||||
it 'instruments pipelined request count' do
|
||||
expect(Gitlab::Instrumentation::Redis::Cache).to receive(:instance_count_pipelined_request)
|
||||
|
||||
minimal_test_class_instance.check_command([[:get, '{user1}:bar'], [:get, '{user1}:foo']], true)
|
||||
instrumented_command
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -117,6 +117,8 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :request_store, featur
|
|||
expect do
|
||||
redis_store_class.with { |redis| redis.call(:auth, 'foo', 'bar') }
|
||||
end.to raise_exception(Redis::CommandError)
|
||||
|
||||
expect(Thread.current[:redis_client_error_count]).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Patch::RedisClient, feature_category: :redis do
|
||||
include RedisHelpers
|
||||
|
||||
let_it_be(:redis_store_class) { define_helper_redis_store_class }
|
||||
let_it_be(:redis_client) { RedisClient.new(redis_store_class.redis_client_params) }
|
||||
|
||||
before do
|
||||
Thread.current[:redis_client_error_count] = 1
|
||||
end
|
||||
|
||||
it 'resets tracking count after each call' do
|
||||
expect { redis_client.call("ping") }
|
||||
.to change { Thread.current[:redis_client_error_count] }
|
||||
.from(1).to(0)
|
||||
end
|
||||
|
||||
it 'resets tracking count after each blocking call' do
|
||||
expect { redis_client.blocking_call(false, "ping") }
|
||||
.to change { Thread.current[:redis_client_error_count] }
|
||||
.from(1).to(0)
|
||||
end
|
||||
|
||||
it 'resets tracking count after pipelined' do
|
||||
expect { redis_client.pipelined { |p| p.call("ping") } }
|
||||
.to change { Thread.current[:redis_client_error_count] }
|
||||
.from(1).to(0)
|
||||
end
|
||||
|
||||
it 'resets tracking count after multi' do
|
||||
expect { redis_client.multi { |p| p.call("ping") } }
|
||||
.to change { Thread.current[:redis_client_error_count] }
|
||||
.from(1).to(0)
|
||||
end
|
||||
end
|
||||
|
|
@ -42,7 +42,7 @@ RSpec.describe Gitlab::ProcessSupervisor, feature_category: :cloud_connector do
|
|||
pids_killed = []
|
||||
|
||||
supervisor.supervise(process_ids) do |dead_pids|
|
||||
pids_killed = dead_pids
|
||||
pids_killed += dead_pids
|
||||
[]
|
||||
end
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ RSpec.describe Gitlab::ProcessSupervisor, feature_category: :cloud_connector do
|
|||
pids_killed = []
|
||||
|
||||
supervisor.supervise(process_ids) do |dead_pids|
|
||||
pids_killed = dead_pids
|
||||
pids_killed += dead_pids
|
||||
[42] # Fake starting a new process in place of the terminated one.
|
||||
end
|
||||
|
||||
|
|
@ -68,7 +68,7 @@ RSpec.describe Gitlab::ProcessSupervisor, feature_category: :cloud_connector do
|
|||
Process.kill('TERM', process_ids.first)
|
||||
|
||||
await_condition(sleep_sec: health_check_interval_seconds) do
|
||||
pids_killed == [process_ids.first]
|
||||
pids_killed.include?(process_ids.first)
|
||||
end
|
||||
|
||||
expect(Gitlab::ProcessManagement.process_alive?(process_ids.first)).to be(false)
|
||||
|
|
@ -81,7 +81,7 @@ RSpec.describe Gitlab::ProcessSupervisor, feature_category: :cloud_connector do
|
|||
pids_killed = []
|
||||
|
||||
supervisor.supervise(process_ids) do |dead_pids|
|
||||
pids_killed = dead_pids
|
||||
pids_killed += dead_pids
|
||||
# Fake a new process having the same pid as one that was just terminated.
|
||||
[process_ids.last]
|
||||
end
|
||||
|
|
@ -90,7 +90,7 @@ RSpec.describe Gitlab::ProcessSupervisor, feature_category: :cloud_connector do
|
|||
Process.kill('TERM', process_ids.first)
|
||||
|
||||
await_condition(sleep_sec: health_check_interval_seconds) do
|
||||
pids_killed == [process_ids.first]
|
||||
pids_killed.include?(process_ids.first)
|
||||
end
|
||||
|
||||
expect(supervisor.supervised_pids).to contain_exactly(process_ids.last)
|
||||
|
|
@ -101,7 +101,7 @@ RSpec.describe Gitlab::ProcessSupervisor, feature_category: :cloud_connector do
|
|||
pids_killed = []
|
||||
|
||||
supervisor.supervise(process_ids) do |dead_pids|
|
||||
pids_killed = dead_pids
|
||||
pids_killed += dead_pids
|
||||
42
|
||||
end
|
||||
|
||||
|
|
@ -109,7 +109,7 @@ RSpec.describe Gitlab::ProcessSupervisor, feature_category: :cloud_connector do
|
|||
Process.kill('TERM', process_ids.first)
|
||||
|
||||
await_condition(sleep_sec: health_check_interval_seconds) do
|
||||
pids_killed == [process_ids.first]
|
||||
pids_killed.include?(process_ids.first)
|
||||
end
|
||||
|
||||
expect(supervisor.supervised_pids).to contain_exactly(42, process_ids.last)
|
||||
|
|
|
|||
|
|
@ -1,80 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_redis_shared_state do
|
||||
let(:user) { build(:user, id: 1) }
|
||||
let(:user2) { build(:user, id: 2) }
|
||||
let(:user3) { build(:user, id: 3) }
|
||||
let(:project) { build(:project) }
|
||||
let(:namespace) { project.namespace }
|
||||
let(:time) { Time.zone.now }
|
||||
|
||||
shared_examples 'tracks and counts action' do
|
||||
subject { track_action(author: user, project: project) }
|
||||
|
||||
before do
|
||||
stub_application_setting(usage_ping_enabled: true)
|
||||
end
|
||||
|
||||
specify do
|
||||
aggregate_failures do
|
||||
track_action(author: user, project: project)
|
||||
track_action(author: user2, project: project)
|
||||
track_action(author: user3, project: project)
|
||||
|
||||
expect(count_unique(date_from: time.beginning_of_week, date_to: 1.week.from_now)).to eq(3)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'internal event tracking'
|
||||
|
||||
it 'does not track edit actions if author is not present' do
|
||||
track_action(author: nil, project: project)
|
||||
|
||||
expect(count_unique(date_from: time.beginning_of_week, date_to: 1.week.from_now)).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for web IDE edit actions' do
|
||||
let(:event) { described_class::EDIT_BY_WEB_IDE }
|
||||
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
def track_action(params)
|
||||
described_class.track_web_ide_edit_action(**params)
|
||||
end
|
||||
|
||||
def count_unique(params)
|
||||
described_class.count_web_ide_edit_actions(**params)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for SFE edit actions' do
|
||||
let(:event) { described_class::EDIT_BY_SFE }
|
||||
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
def track_action(params)
|
||||
described_class.track_sfe_edit_action(**params)
|
||||
end
|
||||
|
||||
def count_unique(params)
|
||||
described_class.count_sfe_edit_actions(**params)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for snippet editor edit actions' do
|
||||
let(:event) { described_class::EDIT_BY_SNIPPET_EDITOR }
|
||||
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
def track_action(params)
|
||||
described_class.track_snippet_editor_edit_action(**params)
|
||||
end
|
||||
|
||||
def count_unique(params)
|
||||
described_class.count_snippet_editor_edit_actions(**params)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -62,15 +62,15 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
|
|||
|
||||
describe 'known_events' do
|
||||
let(:weekly_event) { 'g_analytics_contribution' }
|
||||
let(:daily_event) { 'g_analytics_search' }
|
||||
let(:daily_event) { 'g_analytics_issues' }
|
||||
let(:analytics_slot_event) { 'g_analytics_contribution' }
|
||||
let(:compliance_slot_event) { 'g_compliance_dashboard' }
|
||||
let(:category_analytics_event) { 'g_analytics_search' }
|
||||
let(:category_analytics_event) { 'g_analytics_issues' }
|
||||
let(:category_productivity_event) { 'g_analytics_productivity' }
|
||||
let(:no_slot) { 'no_slot' }
|
||||
let(:different_aggregation) { 'different_aggregation' }
|
||||
let(:custom_daily_event) { 'g_analytics_custom' }
|
||||
|
||||
let(:event_overridden_for_user) { 'user_created_custom_dashboard' }
|
||||
let(:global_category) { 'global' }
|
||||
let(:compliance_category) { 'compliance' }
|
||||
let(:productivity_category) { 'productivity' }
|
||||
|
|
@ -84,7 +84,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
|
|||
{ name: category_productivity_event },
|
||||
{ name: compliance_slot_event },
|
||||
{ name: no_slot },
|
||||
{ name: different_aggregation }
|
||||
{ name: different_aggregation },
|
||||
{ name: event_overridden_for_user }
|
||||
].map(&:with_indifferent_access)
|
||||
end
|
||||
|
||||
|
|
@ -216,6 +217,97 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "property_name" do
|
||||
before do
|
||||
stub_feature_flags(redis_hll_property_name_tracking: property_name_flag_enabled)
|
||||
end
|
||||
|
||||
context "with enabled feature flag" do
|
||||
let(:property_name_flag_enabled) { true }
|
||||
|
||||
context "with a property_name for an overridden event" do
|
||||
context "with a property_name sent as a symbol" do
|
||||
it "tracks the events using the Redis key override" do
|
||||
expected_key = "{hll_counters}_#{event_overridden_for_user}-2020-23"
|
||||
expect(Gitlab::Redis::HLL).to receive(:add).with(hash_including(key: expected_key))
|
||||
|
||||
described_class.track_event(event_overridden_for_user, values: entity1, property_name: :user)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a property_name sent in string format" do
|
||||
it "tracks the events using the Redis key override" do
|
||||
expected_key = "{hll_counters}_#{event_overridden_for_user}-2020-23"
|
||||
expect(Gitlab::Redis::HLL).to receive(:add).with(hash_including(key: expected_key))
|
||||
|
||||
described_class.track_event(event_overridden_for_user, values: entity1, property_name: 'user.id')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with a property_name for an overridden event that doesn't include this property_name" do
|
||||
it "tracks the events using a Redis key with the property_name" do
|
||||
expected_key = "{hll_counters}_#{no_slot}-user-2020-23"
|
||||
expect(Gitlab::Redis::HLL).to receive(:add).with(hash_including(key: expected_key))
|
||||
|
||||
described_class.track_event(no_slot, values: entity1, property_name: 'user')
|
||||
end
|
||||
end
|
||||
|
||||
context "with a property_name for a new event" do
|
||||
it "tracks the events using a Redis key with the property_name" do
|
||||
expected_key = "{hll_counters}_#{no_slot}-project-2020-23"
|
||||
expect(Gitlab::Redis::HLL).to receive(:add).with(hash_including(key: expected_key))
|
||||
|
||||
described_class.track_event(no_slot, values: entity1, property_name: 'project')
|
||||
end
|
||||
end
|
||||
|
||||
context "with no property_name for an overridden event" do
|
||||
it "tracks the events using a Redis key with no property_name" do
|
||||
expected_key = "{hll_counters}_#{event_overridden_for_user}-2020-23"
|
||||
expect(Gitlab::Redis::HLL).to receive(:add).with(hash_including(key: expected_key))
|
||||
|
||||
described_class.track_event(event_overridden_for_user, values: entity1)
|
||||
end
|
||||
end
|
||||
|
||||
context "with no property_name for a new event" do
|
||||
it "tracks the events using a Redis key with no property_name" do
|
||||
expected_key = "{hll_counters}_#{no_slot}-2020-23"
|
||||
expect(Gitlab::Redis::HLL).to receive(:add).with(hash_including(key: expected_key))
|
||||
|
||||
described_class.track_event(no_slot, values: entity1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with disabled feature flag" do
|
||||
let(:property_name_flag_enabled) { false }
|
||||
|
||||
it "uses old Redis key for overridden events" do
|
||||
expected_key = "{hll_counters}_#{event_overridden_for_user}-2020-23"
|
||||
expect(Gitlab::Redis::HLL).to receive(:add).with(hash_including(key: expected_key))
|
||||
|
||||
described_class.track_event(event_overridden_for_user, values: entity1, property_name: 'user')
|
||||
end
|
||||
|
||||
it "uses old Redis key for new events" do
|
||||
expected_key = "{hll_counters}_#{no_slot}-2020-23"
|
||||
expect(Gitlab::Redis::HLL).to receive(:add).with(hash_including(key: expected_key))
|
||||
|
||||
described_class.track_event(no_slot, values: entity1, property_name: 'project')
|
||||
end
|
||||
|
||||
it "uses old Redis key for new events when no property name sent" do
|
||||
expected_key = "{hll_counters}_#{no_slot}-2020-23"
|
||||
expect(Gitlab::Redis::HLL).to receive(:add).with(hash_including(key: expected_key))
|
||||
|
||||
described_class.track_event(no_slot, values: entity1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.unique_events' do
|
||||
|
|
@ -227,7 +319,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
|
|||
# Events last week
|
||||
described_class.track_event(weekly_event, values: entity1, time: 2.days.ago)
|
||||
described_class.track_event(weekly_event, values: entity1, time: 2.days.ago)
|
||||
described_class.track_event(no_slot, values: entity1, time: 2.days.ago)
|
||||
described_class.track_event(no_slot, values: entity1, property_name: 'user.id', time: 2.days.ago)
|
||||
|
||||
# Events 2 weeks ago
|
||||
described_class.track_event(weekly_event, values: entity1, time: 2.weeks.ago)
|
||||
|
|
@ -274,7 +366,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
|
|||
end
|
||||
|
||||
context 'when no slot is set' do
|
||||
it { expect(described_class.unique_events(event_names: [no_slot], start_date: 7.days.ago, end_date: Date.current)).to eq(1) }
|
||||
it { expect(described_class.unique_events(event_names: [no_slot], property_name: 'user.id', start_date: 7.days.ago, end_date: Date.current)).to eq(1) }
|
||||
end
|
||||
|
||||
context 'when data crosses into new year' do
|
||||
|
|
@ -283,6 +375,97 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
|
|||
.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
describe "property_names" do
|
||||
before do
|
||||
stub_feature_flags(redis_hll_property_name_tracking: property_name_flag_enabled)
|
||||
end
|
||||
|
||||
context "with enabled feature flag" do
|
||||
let(:property_name_flag_enabled) { true }
|
||||
|
||||
context "with a property_name for an overridden event" do
|
||||
context "with a property_name sent as a symbol" do
|
||||
it "tracks the events using the Redis key override" do
|
||||
expected_key = "{hll_counters}_#{event_overridden_for_user}-2020-22"
|
||||
expect(Gitlab::Redis::HLL).to receive(:count).with(keys: [expected_key])
|
||||
|
||||
described_class.unique_events(event_names: [event_overridden_for_user], property_name: :user, start_date: 7.days.ago, end_date: Date.current)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a property_name sent in string format" do
|
||||
it "tracks the events using the Redis key override" do
|
||||
expected_key = "{hll_counters}_#{event_overridden_for_user}-2020-22"
|
||||
expect(Gitlab::Redis::HLL).to receive(:count).with(keys: [expected_key])
|
||||
|
||||
described_class.unique_events(event_names: [event_overridden_for_user], property_name: 'user.id', start_date: 7.days.ago, end_date: Date.current)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with a property_name for an overridden event that doesn't include this property_name" do
|
||||
it "tracks the events using a Redis key with the property_name" do
|
||||
expected_key = "{hll_counters}_#{no_slot}-user-2020-22"
|
||||
expect(Gitlab::Redis::HLL).to receive(:count).with(keys: [expected_key])
|
||||
|
||||
described_class.unique_events(event_names: [no_slot], property_name: 'user', start_date: 7.days.ago, end_date: Date.current)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a property_name for a new event" do
|
||||
it "tracks the events using a Redis key with the property_name" do
|
||||
expected_key = "{hll_counters}_#{no_slot}-project-2020-22"
|
||||
expect(Gitlab::Redis::HLL).to receive(:count).with(keys: [expected_key])
|
||||
|
||||
described_class.unique_events(event_names: [no_slot], property_name: 'project', start_date: 7.days.ago, end_date: Date.current)
|
||||
end
|
||||
end
|
||||
|
||||
context "with no property_name for a overridden event" do
|
||||
it "tracks the events using a Redis key with no property_name" do
|
||||
expected_key = "{hll_counters}_#{event_overridden_for_user}-2020-22"
|
||||
expect(Gitlab::Redis::HLL).to receive(:count).with(keys: [expected_key])
|
||||
|
||||
described_class.unique_events(event_names: [event_overridden_for_user], start_date: 7.days.ago, end_date: Date.current)
|
||||
end
|
||||
end
|
||||
|
||||
context "with no property_name for a new event" do
|
||||
it "tracks the events using a Redis key with no property_name" do
|
||||
expected_key = "{hll_counters}_#{no_slot}-2020-22"
|
||||
expect(Gitlab::Redis::HLL).to receive(:count).with(keys: [expected_key])
|
||||
|
||||
described_class.unique_events(event_names: [no_slot], start_date: 7.days.ago, end_date: Date.current)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with disabled feature flag" do
|
||||
let(:property_name_flag_enabled) { false }
|
||||
|
||||
it "uses old Redis key for overridden events" do
|
||||
expected_key = "{hll_counters}_#{event_overridden_for_user}-2020-22"
|
||||
expect(Gitlab::Redis::HLL).to receive(:count).with(keys: [expected_key])
|
||||
|
||||
described_class.unique_events(event_names: [event_overridden_for_user], property_name: 'user', start_date: 7.days.ago, end_date: Date.current)
|
||||
end
|
||||
|
||||
it "uses old Redis key for new events" do
|
||||
expected_key = "{hll_counters}_#{no_slot}-2020-22"
|
||||
expect(Gitlab::Redis::HLL).to receive(:count).with(keys: [expected_key])
|
||||
|
||||
described_class.unique_events(event_names: [no_slot], property_name: 'project', start_date: 7.days.ago, end_date: Date.current)
|
||||
end
|
||||
|
||||
it "uses old Redis key for new events when no property name sent" do
|
||||
expected_key = "{hll_counters}_#{no_slot}-2020-22"
|
||||
expect(Gitlab::Redis::HLL).to receive(:count).with(keys: [expected_key])
|
||||
|
||||
described_class.unique_events(event_names: [no_slot], start_date: 7.days.ago, end_date: Date.current)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'key overrides file' do
|
||||
|
|
@ -341,43 +524,43 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
|
|||
let(:time_range) { { start_date: 7.days.ago, end_date: DateTime.current } }
|
||||
let(:known_events) do
|
||||
[
|
||||
{ name: 'event1_slot' },
|
||||
{ name: 'event2_slot' },
|
||||
{ name: 'event3_slot' },
|
||||
{ name: 'event5_slot' },
|
||||
{ name: 'event4' }
|
||||
{ name: 'g_compliance_dashboard' },
|
||||
{ name: 'g_project_management_epic_created' },
|
||||
{ name: 'g_project_management_epic_closed' },
|
||||
{ name: 'g_project_management_epic_reopened' },
|
||||
{ name: 'g_project_management_epic_issue_added' }
|
||||
].map(&:with_indifferent_access)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(described_class).to receive(:known_events).and_return(known_events)
|
||||
|
||||
described_class.track_event('event1_slot', values: entity1, time: 2.days.ago)
|
||||
described_class.track_event('event1_slot', values: entity2, time: 2.days.ago)
|
||||
described_class.track_event('event1_slot', values: entity3, time: 2.days.ago)
|
||||
described_class.track_event('event2_slot', values: entity1, time: 2.days.ago)
|
||||
described_class.track_event('event2_slot', values: entity2, time: 3.days.ago)
|
||||
described_class.track_event('event2_slot', values: entity3, time: 3.days.ago)
|
||||
described_class.track_event('event3_slot', values: entity1, time: 3.days.ago)
|
||||
described_class.track_event('event3_slot', values: entity2, time: 3.days.ago)
|
||||
described_class.track_event('event5_slot', values: entity2, time: 3.days.ago)
|
||||
described_class.track_event('g_compliance_dashboard', values: entity1, time: 2.days.ago)
|
||||
described_class.track_event('g_compliance_dashboard', values: entity2, time: 2.days.ago)
|
||||
described_class.track_event('g_compliance_dashboard', values: entity3, time: 2.days.ago)
|
||||
described_class.track_event('g_project_management_epic_created', values: entity1, time: 2.days.ago)
|
||||
described_class.track_event('g_project_management_epic_created', values: entity2, time: 3.days.ago)
|
||||
described_class.track_event('g_project_management_epic_created', values: entity3, time: 3.days.ago)
|
||||
described_class.track_event('g_project_management_epic_closed', values: entity1, time: 3.days.ago)
|
||||
described_class.track_event('g_project_management_epic_closed', values: entity2, time: 3.days.ago)
|
||||
described_class.track_event('g_project_management_epic_reopened', values: entity2, time: 3.days.ago)
|
||||
|
||||
# events out of time scope
|
||||
described_class.track_event('event2_slot', values: entity4, time: 8.days.ago)
|
||||
described_class.track_event('g_project_management_epic_created', values: entity4, time: 8.days.ago)
|
||||
|
||||
# events in different slots
|
||||
described_class.track_event('event4', values: entity1, time: 2.days.ago)
|
||||
described_class.track_event('event4', values: entity2, time: 2.days.ago)
|
||||
described_class.track_event('g_project_management_epic_issue_added', values: entity1, time: 2.days.ago)
|
||||
described_class.track_event('g_project_management_epic_issue_added', values: entity2, time: 2.days.ago)
|
||||
end
|
||||
|
||||
it 'calculates union of given events', :aggregate_failures do
|
||||
expect(described_class.calculate_events_union(**time_range.merge(event_names: %w[event4]))).to eq 2
|
||||
expect(described_class.calculate_events_union(**time_range.merge(event_names: %w[event1_slot event2_slot event3_slot]))).to eq 3
|
||||
expect(described_class.calculate_events_union(**time_range.merge(event_names: %w[g_project_management_epic_issue_added]))).to eq 2
|
||||
expect(described_class.calculate_events_union(**time_range.merge(event_names: %w[g_compliance_dashboard g_project_management_epic_created g_project_management_epic_closed]))).to eq 3
|
||||
end
|
||||
|
||||
it 'returns 0 if there are no keys for given events' do
|
||||
expect(Gitlab::Redis::HLL).not_to receive(:count)
|
||||
expect(described_class.calculate_events_union(event_names: %w[event1_slot event2_slot event3_slot], start_date: Date.current, end_date: 4.weeks.ago)).to eq(-1)
|
||||
expect(described_class.calculate_events_union(event_names: %w[g_compliance_dashboard g_project_management_epic_created g_project_management_epic_closed], start_date: Date.current, end_date: 4.weeks.ago)).to eq(-1)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1122,6 +1122,50 @@ RSpec.describe Group, feature_category: :groups_and_projects do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.excluding_restricted_visibility_levels_for_user' do
|
||||
let_it_be(:admin_user) { create(:admin) }
|
||||
|
||||
context 'when restricted_visibility_level is not configured' do
|
||||
context 'when user is an admin', :enable_admin_mode do
|
||||
it 'returns all groups' do
|
||||
expect(described_class.excluding_restricted_visibility_levels_for_user(admin_user)).to eq(
|
||||
[private_group, internal_group, group]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not an admin' do
|
||||
it 'returns all groups' do
|
||||
expect(described_class.excluding_restricted_visibility_levels_for_user(user1)).to eq(
|
||||
[private_group, internal_group, group]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when restricted_visibility_level is set to private' do
|
||||
before do
|
||||
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PRIVATE])
|
||||
end
|
||||
|
||||
context 'and user is an admin', :enable_admin_mode do
|
||||
it 'returns all groups' do
|
||||
expect(described_class.excluding_restricted_visibility_levels_for_user(admin_user)).to eq(
|
||||
[private_group, internal_group, group]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'and user is not an admin' do
|
||||
it 'excludes private groups' do
|
||||
expect(described_class.excluding_restricted_visibility_levels_for_user(user1)).to eq(
|
||||
[internal_group, group]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.project_creation_allowed' do
|
||||
let_it_be(:group_1) { create(:group, project_creation_level: Gitlab::Access::NO_ONE_PROJECT_ACCESS) }
|
||||
let_it_be(:group_2) { create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) }
|
||||
|
|
@ -1129,7 +1173,9 @@ RSpec.describe Group, feature_category: :groups_and_projects do
|
|||
let_it_be(:group_4) { create(:group, project_creation_level: nil) }
|
||||
|
||||
it 'only includes groups where project creation is allowed' do
|
||||
result = described_class.project_creation_allowed
|
||||
expect(described_class).to receive(:excluding_restricted_visibility_levels_for_user).and_call_original
|
||||
|
||||
result = described_class.project_creation_allowed(user1)
|
||||
|
||||
expect(result).to include(group_2, group_3, group_4)
|
||||
expect(result).not_to include(group_1)
|
||||
|
|
@ -1141,7 +1187,9 @@ RSpec.describe Group, feature_category: :groups_and_projects do
|
|||
end
|
||||
|
||||
it 'only includes groups where project creation is allowed' do
|
||||
result = described_class.project_creation_allowed
|
||||
expect(described_class).to receive(:excluding_restricted_visibility_levels_for_user).and_call_original
|
||||
|
||||
result = described_class.project_creation_allowed(user1)
|
||||
|
||||
expect(result).to include(group_2, group_3)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,31 @@ RSpec.describe Projects::BranchRule, feature_category: :source_code_management d
|
|||
|
||||
subject { described_class.new(protected_branch.project, protected_branch) }
|
||||
|
||||
describe '::find(id)' do
|
||||
context 'when id matches a Project' do
|
||||
it 'finds the project and initializes a branch rule' do
|
||||
instance = described_class.find(protected_branch.id)
|
||||
expect(instance).to be_instance_of(described_class)
|
||||
expect(instance.protected_branch.id).to eq(protected_branch.id)
|
||||
expect(instance.project.id).to eq(project.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when id does not match a Project' do
|
||||
it 'raises an ActiveRecord::RecordNotFound error describing the branch rule' do
|
||||
expect { described_class.find(0) }.to raise_error(
|
||||
ActiveRecord::RecordNotFound, "Couldn't find Projects::BranchRule with 'id'=0"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'generates a valid global id' do
|
||||
expect(subject.to_global_id.to_s).to eq("gid://gitlab/Projects::BranchRule/#{protected_branch.id}")
|
||||
end
|
||||
|
||||
it 'delegates methods to protected branch' do
|
||||
expect(subject).to delegate_method(:id).to(:protected_branch)
|
||||
expect(subject).to delegate_method(:name).to(:protected_branch)
|
||||
expect(subject).to delegate_method(:group).to(:protected_branch)
|
||||
expect(subject).to delegate_method(:default_branch?).to(:protected_branch)
|
||||
|
|
|
|||
|
|
@ -581,7 +581,7 @@ RSpec.describe API::Commits, feature_category: :source_code_management do
|
|||
context 'when using access token authentication' do
|
||||
it 'does not increment the usage counters' do
|
||||
expect(::Gitlab::UsageDataCounters::WebIdeCounter).not_to receive(:increment_commits_count)
|
||||
expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).not_to receive(:track_web_ide_edit_action)
|
||||
expect(::Gitlab::InternalEvents).not_to receive(:track_event)
|
||||
|
||||
post api(url, user), params: valid_c_params
|
||||
end
|
||||
|
|
@ -596,21 +596,16 @@ RSpec.describe API::Commits, feature_category: :source_code_management do
|
|||
|
||||
it 'increments usage counters' do
|
||||
expect(::Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_commits_count)
|
||||
expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_web_ide_edit_action)
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it_behaves_like 'internal event tracking' do
|
||||
let(:event) { ::Gitlab::UsageDataCounters::EditorUniqueCounter::EDIT_BY_WEB_IDE }
|
||||
let(:event) { 'g_edit_by_web_ide' }
|
||||
let(:namespace) { project.namespace.reload }
|
||||
end
|
||||
|
||||
context 'counts.web_ide_commits Snowplow event tracking' do
|
||||
before do
|
||||
allow(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_web_ide_edit_action)
|
||||
end
|
||||
|
||||
it_behaves_like 'Snowplow event tracking' do
|
||||
let(:action) { :commit }
|
||||
let(:category) { described_class.to_s }
|
||||
|
|
|
|||
|
|
@ -106,7 +106,9 @@ RSpec.describe 'Creating a Snippet', feature_category: :source_code_management d
|
|||
end
|
||||
|
||||
context 'with PersonalSnippet' do
|
||||
it_behaves_like 'creates snippet'
|
||||
it_behaves_like 'creates snippet' do
|
||||
let(:project) { nil }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with ProjectSnippet' do
|
||||
|
|
|
|||
|
|
@ -43,7 +43,8 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d
|
|||
|
||||
shared_examples 'graphql update actions' do
|
||||
context 'when the user does not have permission' do
|
||||
let(:current_user) { create(:user) }
|
||||
let(:user) { create(:user) }
|
||||
let(:current_user) { user }
|
||||
|
||||
it_behaves_like 'a mutation that returns top-level errors',
|
||||
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
|
||||
|
|
@ -131,14 +132,18 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d
|
|||
|
||||
it_behaves_like 'graphql update actions'
|
||||
it_behaves_like 'when the snippet is not found'
|
||||
it_behaves_like 'snippet edit usage data counters'
|
||||
it_behaves_like 'snippet edit usage data counters' do
|
||||
let(:user) { current_user }
|
||||
end
|
||||
|
||||
it_behaves_like 'has spam protection' do
|
||||
let(:mutation_class) { ::Mutations::Snippets::Update }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'ProjectSnippet' do
|
||||
let_it_be(:project) { create(:project, :private) }
|
||||
let_it_be(:namespace) { create(:namespace) }
|
||||
let_it_be(:project) { create(:project, :private, namespace: namespace) }
|
||||
|
||||
let(:snippet) do
|
||||
create(
|
||||
|
|
@ -181,7 +186,9 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d
|
|||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'snippet edit usage data counters'
|
||||
it_behaves_like 'snippet edit usage data counters' do
|
||||
let(:user) { current_user }
|
||||
end
|
||||
|
||||
it_behaves_like 'has spam protection' do
|
||||
let(:mutation_class) { ::Mutations::Snippets::Update }
|
||||
|
|
@ -193,9 +200,8 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d
|
|||
end
|
||||
|
||||
it_behaves_like 'internal event tracking' do
|
||||
let(:event) { ::Gitlab::UsageDataCounters::EditorUniqueCounter::EDIT_BY_SNIPPET_EDITOR }
|
||||
let(:event) { 'g_edit_by_snippet_ide' }
|
||||
let(:user) { current_user }
|
||||
let(:namespace) { project.namespace }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -330,6 +330,9 @@ RSpec.configure do |config|
|
|||
|
||||
# Postgres is the primary data source, and ClickHouse only when enabled in certain cases.
|
||||
stub_feature_flags(clickhouse_data_collection: false)
|
||||
|
||||
# This is going to be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/432866
|
||||
stub_feature_flags(redis_hll_property_name_tracking: false)
|
||||
else
|
||||
unstub_all_feature_flags
|
||||
end
|
||||
|
|
|
|||
|
|
@ -125,9 +125,6 @@ project_compliance_standards_adherence:
|
|||
project_repositories:
|
||||
index_project_repositories_on_shard_id_and_project_id:
|
||||
- index_project_repositories_on_shard_id
|
||||
protected_environments:
|
||||
index_protected_environments_on_project_id_and_name:
|
||||
- index_protected_environments_on_project_id
|
||||
protected_tags:
|
||||
index_protected_tags_on_project_id_and_name:
|
||||
- index_protected_tags_on_project_id
|
||||
|
|
|
|||
|
|
@ -6727,7 +6727,6 @@
|
|||
- './spec/lib/gitlab/usage_data_counters/code_review_events_spec.rb'
|
||||
- './spec/lib/gitlab/usage_data_counters/cycle_analytics_counter_spec.rb'
|
||||
- './spec/lib/gitlab/usage_data_counters/designs_counter_spec.rb'
|
||||
- './spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb'
|
||||
- './spec/lib/gitlab/usage_data_counters/gitlab_cli_activity_unique_counter_spec.rb'
|
||||
- './spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb'
|
||||
- './spec/lib/gitlab/usage_data_counters/ipynb_diff_activity_counter_spec.rb'
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ RSpec.shared_examples 'internal event tracking' do
|
|||
|
||||
project = try(:project)
|
||||
user = try(:user)
|
||||
namespace = try(:namespace)
|
||||
namespace = try(:namespace) || project&.namespace
|
||||
|
||||
expect(Gitlab::Tracking::StandardContext)
|
||||
.to have_received(:new)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ RSpec.shared_examples 'snippet edit usage data counters' do
|
|||
|
||||
context 'when user is sessionless' do
|
||||
it 'does not track usage data actions' do
|
||||
expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).not_to receive(:track_snippet_editor_edit_action)
|
||||
expect(::Gitlab::InternalEvents).not_to receive(:track_event)
|
||||
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
end
|
||||
|
|
@ -25,17 +25,19 @@ RSpec.shared_examples 'snippet edit usage data counters' do
|
|||
stub_session('warden.user.user.key' => [[current_user.id], current_user.authenticatable_salt])
|
||||
end
|
||||
|
||||
it 'tracks usage data actions', :clean_gitlab_redis_sessions do
|
||||
expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_snippet_editor_edit_action)
|
||||
|
||||
subject do
|
||||
post_graphql_mutation(mutation)
|
||||
end
|
||||
|
||||
it_behaves_like 'internal event tracking' do
|
||||
let(:event) { 'g_edit_by_snippet_ide' }
|
||||
end
|
||||
|
||||
context 'when mutation result raises an error' do
|
||||
it 'does not track usage data actions' do
|
||||
mutation_vars[:title] = nil
|
||||
|
||||
expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).not_to receive(:track_snippet_editor_edit_action)
|
||||
expect(::Gitlab::InternalEvents).not_to receive(:track_event)
|
||||
|
||||
post_graphql_mutation(mutation)
|
||||
end
|
||||
|
|
|
|||