diff options
53 files changed, 951 insertions, 434 deletions
@@ -477,7 +477,7 @@ gem 'flipper', '~> 0.17.1' gem 'flipper-active_record', '~> 0.17.1' gem 'flipper-active_support_cache_store', '~> 0.17.1' gem 'unleash', '~> 0.1.5' -gem 'gitlab-experiment', '~> 0.4.5' +gem 'gitlab-experiment', '~> 0.4.8' # Structured logging gem 'lograge', '~> 0.5' diff --git a/Gemfile.lock b/Gemfile.lock index 5e52a732906..61dfa7937b3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -424,7 +424,7 @@ GEM github-markup (1.7.0) gitlab-chronic (0.10.5) numerizer (~> 0.2) - gitlab-experiment (0.4.5) + gitlab-experiment (0.4.8) activesupport (>= 3.0) scientist (~> 1.5, >= 1.5.0) gitlab-fog-azure-rm (1.0.0) @@ -1364,7 +1364,7 @@ DEPENDENCIES gitaly (~> 13.8.0.pre.rc3) github-markup (~> 1.7.0) gitlab-chronic (~> 0.10.5) - gitlab-experiment (~> 0.4.5) + gitlab-experiment (~> 0.4.8) gitlab-fog-azure-rm (~> 1.0) gitlab-labkit (= 0.14.0) gitlab-license (~> 1.0) diff --git a/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js b/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js index 9d47a1b7132..15f9512fe92 100644 --- a/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js +++ b/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js @@ -20,8 +20,9 @@ function formatNumber( return ''; } + const locale = document.documentElement.lang || undefined; const num = value * valueFactor; - const formatted = num.toLocaleString(undefined, { + const formatted = num.toLocaleString(locale, { minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits, style, diff --git a/app/assets/javascripts/pages/projects/activity/index.js b/app/assets/javascripts/pages/projects/activity/index.js index d39ea3d10bf..03fbad0f1ec 100644 --- a/app/assets/javascripts/pages/projects/activity/index.js +++ b/app/assets/javascripts/pages/projects/activity/index.js @@ -1,7 +1,5 @@ import Activities from '~/activities'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; -document.addEventListener('DOMContentLoaded', () => { - new Activities(); // eslint-disable-line no-new - new ShortcutsNavigation(); // eslint-disable-line no-new -}); +new Activities(); // eslint-disable-line no-new +new ShortcutsNavigation(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue index 61b899896bc..eeb3fda5272 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue @@ -1,47 +1,12 @@ <script> -import { GlAlert, GlTabs, GlTab } from '@gitlab/ui'; -import { s__ } from '~/locale'; -import getPipelineCountByStatus from '../graphql/queries/get_pipeline_count_by_status.query.graphql'; -import getProjectPipelineStatistics from '../graphql/queries/get_project_pipeline_statistics.query.graphql'; +import { GlTabs, GlTab } from '@gitlab/ui'; import PipelineCharts from './pipeline_charts.vue'; import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility'; -import { - DEFAULT, - LOAD_ANALYTICS_FAILURE, - LOAD_PIPELINES_FAILURE, - PARSE_FAILURE, - UNSUPPORTED_DATA, -} from '../constants'; - -const defaultAnalyticsValues = { - weekPipelinesTotals: [], - weekPipelinesLabels: [], - weekPipelinesSuccessful: [], - monthPipelinesLabels: [], - monthPipelinesTotals: [], - monthPipelinesSuccessful: [], - yearPipelinesLabels: [], - yearPipelinesTotals: [], - yearPipelinesSuccessful: [], - pipelineTimesLabels: [], - pipelineTimesValues: [], -}; - -const defaultCountValues = { - totalPipelines: { - count: 0, - }, - successfulPipelines: { - count: 0, - }, -}; - const charts = ['pipelines', 'deployments']; export default { components: { - GlAlert, GlTabs, GlTab, PipelineCharts, @@ -53,10 +18,6 @@ export default { type: Boolean, default: false, }, - projectPath: { - type: String, - default: '', - }, }, data() { const [chart] = getParameterValues('chart') || charts; @@ -64,169 +25,27 @@ export default { return { chart, selectedTab: tab >= 0 ? tab : 0, - showFailureAlert: false, - failureType: null, - analytics: { ...defaultAnalyticsValues }, - counts: { ...defaultCountValues }, }; }, - apollo: { - counts: { - query: getPipelineCountByStatus, - variables() { - return { - projectPath: this.projectPath, - }; - }, - update(data) { - return data?.project; - }, - error() { - this.reportFailure(LOAD_PIPELINES_FAILURE); - }, - }, - analytics: { - query: getProjectPipelineStatistics, - variables() { - return { - projectPath: this.projectPath, - }; - }, - update(data) { - return data?.project?.pipelineAnalytics; - }, - error() { - this.reportFailure(LOAD_ANALYTICS_FAILURE); - }, - }, - }, - computed: { - failure() { - switch (this.failureType) { - case LOAD_ANALYTICS_FAILURE: - return { - text: this.$options.errorTexts[LOAD_ANALYTICS_FAILURE], - variant: 'danger', - }; - case PARSE_FAILURE: - return { - text: this.$options.errorTexts[PARSE_FAILURE], - variant: 'danger', - }; - case UNSUPPORTED_DATA: - return { - text: this.$options.errorTexts[UNSUPPORTED_DATA], - variant: 'info', - }; - default: - return { - text: this.$options.errorTexts[DEFAULT], - variant: 'danger', - }; - } - }, - lastWeekChartData() { - return { - labels: this.analytics.weekPipelinesLabels, - totals: this.analytics.weekPipelinesTotals, - success: this.analytics.weekPipelinesSuccessful, - }; - }, - lastMonthChartData() { - return { - labels: this.analytics.monthPipelinesLabels, - totals: this.analytics.monthPipelinesTotals, - success: this.analytics.monthPipelinesSuccessful, - }; - }, - lastYearChartData() { - return { - labels: this.analytics.yearPipelinesLabels, - totals: this.analytics.yearPipelinesTotals, - success: this.analytics.yearPipelinesSuccessful, - }; - }, - timesChartData() { - return { - labels: this.analytics.pipelineTimesLabels, - values: this.analytics.pipelineTimesValues, - }; - }, - successRatio() { - const { successfulPipelines, failedPipelines } = this.counts; - const successfulCount = successfulPipelines?.count; - const failedCount = failedPipelines?.count; - const ratio = (successfulCount / (successfulCount + failedCount)) * 100; - - return failedCount === 0 ? 100 : ratio; - }, - formattedCounts() { - const { totalPipelines, successfulPipelines, failedPipelines } = this.counts; - - return { - total: totalPipelines?.count, - success: successfulPipelines?.count, - failed: failedPipelines?.count, - successRatio: this.successRatio, - }; - }, - }, methods: { - hideAlert() { - this.showFailureAlert = false; - }, - reportFailure(type) { - this.showFailureAlert = true; - this.failureType = type; - }, onTabChange(index) { this.selectedTab = index; const path = mergeUrlParams({ chart: charts[index] }, window.location.pathname); updateHistory({ url: path }); }, }, - errorTexts: { - [LOAD_ANALYTICS_FAILURE]: s__( - 'PipelineCharts|An error has ocurred when retrieving the analytics data', - ), - [LOAD_PIPELINES_FAILURE]: s__( - 'PipelineCharts|An error has ocurred when retrieving the pipelines data', - ), - [PARSE_FAILURE]: s__('PipelineCharts|There was an error parsing the data for the charts.'), - [DEFAULT]: s__('PipelineCharts|An unknown error occurred while processing CI/CD analytics.'), - }, }; </script> <template> <div> - <gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="hideAlert">{{ - failure.text - }}</gl-alert> <gl-tabs v-if="shouldRenderDeploymentFrequencyCharts" :value="selectedTab" @input="onTabChange"> <gl-tab :title="__('Pipelines')"> - <pipeline-charts - :counts="formattedCounts" - :last-week="lastWeekChartData" - :last-month="lastMonthChartData" - :last-year="lastYearChartData" - :times-chart="timesChartData" - :loading="$apollo.queries.counts.loading" - @report-failure="reportFailure" - /> + <pipeline-charts /> </gl-tab> <gl-tab :title="__('Deployments')"> <deployment-frequency-charts /> </gl-tab> </gl-tabs> - <pipeline-charts - v-else - :counts="formattedCounts" - :last-week="lastWeekChartData" - :last-month="lastMonthChartData" - :last-year="lastYearChartData" - :times-chart="timesChartData" - :loading="$apollo.queries.counts.loading" - @report-failure="reportFailure" - /> + <pipeline-charts v-else /> </div> </template> diff --git a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue index 369eeb5ce6d..4f1f0382cc9 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue @@ -1,10 +1,13 @@ <script> import dateFormat from 'dateformat'; import { GlColumnChart } from '@gitlab/ui/dist/charts'; -import { GlSkeletonLoader } from '@gitlab/ui'; +import { GlAlert, GlSkeletonLoader } from '@gitlab/ui'; +import getPipelineCountByStatus from '../graphql/queries/get_pipeline_count_by_status.query.graphql'; +import getProjectPipelineStatistics from '../graphql/queries/get_project_pipeline_statistics.query.graphql'; import { __, s__, sprintf } from '~/locale'; import { getDateInPast } from '~/lib/utils/datetime_utility'; import { + DEFAULT, CHART_CONTAINER_HEIGHT, CHART_DATE_FORMAT, INNER_CHART_HEIGHT, @@ -13,51 +16,167 @@ import { X_AXIS_LABEL_ROTATION, X_AXIS_TITLE_OFFSET, PARSE_FAILURE, + LOAD_ANALYTICS_FAILURE, + LOAD_PIPELINES_FAILURE, + UNSUPPORTED_DATA, } from '../constants'; import StatisticsList from './statistics_list.vue'; import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue'; +const defaultAnalyticsValues = { + weekPipelinesTotals: [], + weekPipelinesLabels: [], + weekPipelinesSuccessful: [], + monthPipelinesLabels: [], + monthPipelinesTotals: [], + monthPipelinesSuccessful: [], + yearPipelinesLabels: [], + yearPipelinesTotals: [], + yearPipelinesSuccessful: [], + pipelineTimesLabels: [], + pipelineTimesValues: [], +}; + +const defaultCountValues = { + totalPipelines: { + count: 0, + }, + successfulPipelines: { + count: 0, + }, +}; + export default { components: { + GlAlert, GlColumnChart, GlSkeletonLoader, StatisticsList, CiCdAnalyticsAreaChart, }, - props: { + inject: { + projectPath: { + type: String, + default: '', + }, + }, + data() { + return { + showFailureAlert: false, + failureType: null, + analytics: { ...defaultAnalyticsValues }, + counts: { ...defaultCountValues }, + }; + }, + apollo: { counts: { - required: true, - type: Object, + query: getPipelineCountByStatus, + variables() { + return { + projectPath: this.projectPath, + }; + }, + update(data) { + return data?.project; + }, + error() { + this.reportFailure(LOAD_PIPELINES_FAILURE); + }, }, - loading: { - required: false, - default: false, - type: Boolean, + analytics: { + query: getProjectPipelineStatistics, + variables() { + return { + projectPath: this.projectPath, + }; + }, + update(data) { + return data?.project?.pipelineAnalytics; + }, + error() { + this.reportFailure(LOAD_ANALYTICS_FAILURE); + }, }, - lastWeek: { - required: true, - type: Object, + }, + computed: { + loading() { + return this.$apollo.queries.counts.loading; }, - lastMonth: { - required: true, - type: Object, + failure() { + switch (this.failureType) { + case LOAD_ANALYTICS_FAILURE: + return { + text: this.$options.errorTexts[LOAD_ANALYTICS_FAILURE], + variant: 'danger', + }; + case PARSE_FAILURE: + return { + text: this.$options.errorTexts[PARSE_FAILURE], + variant: 'danger', + }; + case UNSUPPORTED_DATA: + return { + text: this.$options.errorTexts[UNSUPPORTED_DATA], + variant: 'info', + }; + default: + return { + text: this.$options.errorTexts[DEFAULT], + variant: 'danger', + }; + } }, - lastYear: { - required: true, - type: Object, + lastWeekChartData() { + return { + labels: this.analytics.weekPipelinesLabels, + totals: this.analytics.weekPipelinesTotals, + success: this.analytics.weekPipelinesSuccessful, + }; }, - timesChart: { - required: true, - type: Object, + lastMonthChartData() { + return { + labels: this.analytics.monthPipelinesLabels, + totals: this.analytics.monthPipelinesTotals, + success: this.analytics.monthPipelinesSuccessful, + }; + }, + lastYearChartData() { + return { + labels: this.analytics.yearPipelinesLabels, + totals: this.analytics.yearPipelinesTotals, + success: this.analytics.yearPipelinesSuccessful, + }; + }, + timesChartData() { + return { + labels: this.analytics.pipelineTimesLabels, + values: this.analytics.pipelineTimesValues, + }; + }, + successRatio() { + const { successfulPipelines, failedPipelines } = this.counts; + const successfulCount = successfulPipelines?.count; + const failedCount = failedPipelines?.count; + const ratio = (successfulCount / (successfulCount + failedCount)) * 100; + + return failedCount === 0 ? 100 : ratio; + }, + formattedCounts() { + const { totalPipelines, successfulPipelines, failedPipelines } = this.counts; + + return { + total: totalPipelines?.count, + success: successfulPipelines?.count, + failed: failedPipelines?.count, + successRatio: this.successRatio, + }; }, - }, - computed: { areaCharts() { const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles; const charts = [ - { title: lastWeek, data: this.lastWeek }, - { title: lastMonth, data: this.lastMonth }, - { title: lastYear, data: this.lastYear }, + { title: lastWeek, data: this.lastWeekChartData }, + { title: lastMonth, data: this.lastMonthChartData }, + { title: lastYear, data: this.lastYearChartData }, ]; let areaChartsData = []; @@ -65,7 +184,7 @@ export default { areaChartsData = charts.map(this.buildAreaChartData); } catch { areaChartsData = []; - this.vm.$emit('report-failure', PARSE_FAILURE); + this.reportFailure(PARSE_FAILURE); } return areaChartsData; @@ -74,12 +193,19 @@ export default { return [ { name: 'full', - data: this.mergeLabelsAndValues(this.timesChart.labels, this.timesChart.values), + data: this.mergeLabelsAndValues(this.timesChartData.labels, this.timesChartData.values), }, ]; }, }, methods: { + hideAlert() { + this.showFailureAlert = false; + }, + reportFailure(type) { + this.showFailureAlert = true; + this.failureType = type; + }, mergeLabelsAndValues(labels, values) { return labels.map((label, index) => [label, values[index]]); }, @@ -121,6 +247,16 @@ export default { minInterval: 1, }, }, + errorTexts: { + [LOAD_ANALYTICS_FAILURE]: s__( + 'PipelineCharts|An error has ocurred when retrieving the analytics data', + ), + [LOAD_PIPELINES_FAILURE]: s__( + 'PipelineCharts|An error has ocurred when retrieving the pipelines data', + ), + [PARSE_FAILURE]: s__('PipelineCharts|There was an error parsing the data for the charts.'), + [DEFAULT]: s__('PipelineCharts|An unknown error occurred while processing CI/CD analytics.'), + }, get chartTitles() { const today = dateFormat(new Date(), CHART_DATE_FORMAT); const pastDate = (timeScale) => @@ -141,6 +277,9 @@ export default { </script> <template> <div> + <gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="hideAlert">{{ + failure.text + }}</gl-alert> <div class="gl-mb-3"> <h3>{{ s__('PipelineCharts|CI / CD Analytics') }}</h3> </div> @@ -148,7 +287,7 @@ export default { <div class="row"> <div class="col-md-6"> <gl-skeleton-loader v-if="loading" :lines="5" /> - <statistics-list v-else :counts="counts" /> + <statistics-list v-else :counts="formattedCounts" /> </div> <div v-if="!loading" class="col-md-6"> <strong>{{ __('Duration for the last 30 commits') }}</strong> diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb index b9ab1076999..708b7a6c7ba 100644 --- a/app/controllers/projects/discussions_controller.rb +++ b/app/controllers/projects/discussions_controller.rb @@ -18,7 +18,7 @@ class Projects::DiscussionsController < Projects::ApplicationController end def unresolve - discussion.unresolve! + Discussions::UnresolveService.new(discussion, current_user).execute render_discussion end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 9418fda97e0..d3b3341e0b2 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -59,8 +59,7 @@ class Projects::IssuesController < Projects::ApplicationController around_action :allow_gitaly_ref_name_caching, only: [:discussions] before_action :run_null_hypothesis_experiment, - only: [:index, :new, :create], - if: -> { Feature.enabled?(:gitlab_experiments) } + only: [:index, :new, :create] respond_to :html diff --git a/app/experiments/application_experiment.rb b/app/experiments/application_experiment.rb index 7a8851d11ce..9a1a37a6b19 100644 --- a/app/experiments/application_experiment.rb +++ b/app/experiments/application_experiment.rb @@ -1,13 +1,20 @@ # frozen_string_literal: true class ApplicationExperiment < Gitlab::Experiment + def enabled? + return false if Feature::Definition.get(name).nil? # there has to be a feature flag yaml file + return false unless Gitlab.dev_env_or_com? # we're in an environment that allows experiments + + Feature.get(name).state != :off # rubocop:disable Gitlab/AvoidFeatureGet + end + def publish(_result) track(:assignment) # track that we've assigned a variant for this context Gon.global.push({ experiment: { name => signature } }, true) # push to client end def track(action, **event_args) - return if excluded? # no events for opted out actors or excluded subjects + return unless should_track? # no events for opted out actors or excluded subjects Gitlab::Tracking.event(name, action.to_s, **event_args.merge( context: (event_args[:context] || []) << SnowplowTracker::SelfDescribingJson.new( diff --git a/app/graphql/mutations/discussions/toggle_resolve.rb b/app/graphql/mutations/discussions/toggle_resolve.rb index c9834c946b2..6639252ec67 100644 --- a/app/graphql/mutations/discussions/toggle_resolve.rb +++ b/app/graphql/mutations/discussions/toggle_resolve.rb @@ -69,7 +69,7 @@ module Mutations end def unresolve!(discussion) - discussion.unresolve! + ::Discussions::UnresolveService.new(discussion, current_user).execute end end end diff --git a/app/services/discussions/resolve_service.rb b/app/services/discussions/resolve_service.rb index cd5925cd9be..91c3cf136a4 100644 --- a/app/services/discussions/resolve_service.rb +++ b/app/services/discussions/resolve_service.rb @@ -40,7 +40,13 @@ module Discussions discussion.resolve!(current_user) @resolved_count += 1 - MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request) if merge_request + if merge_request + Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter + .track_resolve_thread_action(user: current_user) + + MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request) + end + SystemNoteService.discussion_continued_in_issue(discussion, project, current_user, follow_up_issue) if follow_up_issue end diff --git a/app/services/discussions/unresolve_service.rb b/app/services/discussions/unresolve_service.rb new file mode 100644 index 00000000000..fbd96ceafe7 --- /dev/null +++ b/app/services/discussions/unresolve_service.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Discussions + class UnresolveService < Discussions::BaseService + include Gitlab::Utils::StrongMemoize + + def initialize(discussion, user) + @discussion = discussion + @user = user + + super + end + + def execute + @discussion.unresolve! + + Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter + .track_unresolve_thread_action(user: @user) + end + end +end diff --git a/config/feature_flags/development/gitlab_experiments.yml b/config/feature_flags/development/gitlab_experiments.yml deleted file mode 100644 index 51fa6aa4529..00000000000 --- a/config/feature_flags/development/gitlab_experiments.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: gitlab_experiments -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45840 -rollout_issue_url: -milestone: '13.7' -type: development -group: group::adoption -default_enabled: false diff --git a/config/feature_flags/development/usage_data_i_code_review_user_resolve_thread.yml b/config/feature_flags/development/usage_data_i_code_review_user_resolve_thread.yml new file mode 100644 index 00000000000..8c7af83bab4 --- /dev/null +++ b/config/feature_flags/development/usage_data_i_code_review_user_resolve_thread.yml @@ -0,0 +1,8 @@ +--- +name: usage_data_i_code_review_user_resolve_thread +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52130 +rollout_issue_url: +milestone: '13.9' +type: development +group: group::code review +default_enabled: true diff --git a/config/feature_flags/development/usage_data_i_code_review_user_unresolve_thread.yml b/config/feature_flags/development/usage_data_i_code_review_user_unresolve_thread.yml new file mode 100644 index 00000000000..49b860416c5 --- /dev/null +++ b/config/feature_flags/development/usage_data_i_code_review_user_unresolve_thread.yml @@ -0,0 +1,8 @@ +--- +name: usage_data_i_code_review_user_unresolve_thread +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52130 +rollout_issue_url: +milestone: '13.9' +type: development +group: group::code review +default_enabled: true diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md index cc3421d3133..502d8e4ef91 100644 --- a/doc/administration/auth/README.md +++ b/doc/administration/auth/README.md @@ -22,7 +22,8 @@ providers: - [Facebook](../../integration/facebook.md) - [GitHub](../../integration/github.md) - [GitLab.com](../../integration/gitlab.md) -- [Google](../../integration/google.md) +- [Google OAuth](../../integration/google.md) +- [Google Workspace SSO](../../integration/google_workspace_saml.md) - [JWT](jwt.md) - [Kerberos](../../integration/kerberos.md) - [LDAP](ldap/index.md): Includes Active Directory, Apple Open Directory, Open LDAP, diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md index 1b50e04a003..4313f868710 100644 --- a/doc/development/api_graphql_styleguide.md +++ b/doc/development/api_graphql_styleguide.md @@ -23,6 +23,7 @@ which is exposed as an API endpoint at `/api/graphql`. In March 2019, Nick Thomas hosted a Deep Dive (GitLab team members only: `https://gitlab.com/gitlab-org/create-stage/issues/1`) on the GitLab [GraphQL API](../api/graphql/index.md) to share his domain specific knowledge with anyone who may work in this part of the codebase in the future. You can find the +<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [recording on YouTube](https://www.youtube.com/watch?v=-9L_1MWrjkg), and the slides on [Google Slides](https://docs.google.com/presentation/d/1qOTxpkTdHIp1CRjuTvO-aXg0_rUtzE3ETfLUdnBB5uQ/edit) and in [PDF](https://gitlab.com/gitlab-org/create-stage/uploads/8e78ea7f326b2ef649e7d7d569c26d56/GraphQL_Deep_Dive__Create_.pdf). diff --git a/doc/development/diffs.md b/doc/development/diffs.md index fba8eda0408..7793a4281c8 100644 --- a/doc/development/diffs.md +++ b/doc/development/diffs.md @@ -17,7 +17,7 @@ We rely on different sources to present diffs. These include: In January 2019, Oswaldo Ferreira hosted a Deep Dive (GitLab team members only: `https://gitlab.com/gitlab-org/create-stage/issues/1`) on GitLab Diffs and Commenting on Diffs functionality to share his domain specific knowledge with anyone who may work in this part of the -codebase in the future. You can find the [recording on YouTube](https://www.youtube.com/watch?v=K6G3gMcFyek), +codebase in the future. You can find the <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [recording on YouTube](https://www.youtube.com/watch?v=K6G3gMcFyek), and the slides on [Google Slides](https://docs.google.com/presentation/d/1bGutFH2AT3bxOPZuLMGl1ANWHqFnrxwQwjiwAZkF-TU/edit) and in [PDF](https://gitlab.com/gitlab-org/create-stage/uploads/b5ad2f336e0afcfe0f99db0af0ccc71a/). Everything covered in this deep dive was accurate as of GitLab 11.7, and while specific details may @@ -180,8 +180,8 @@ has been introduced. One of the key challenges to deal with when working on merge ref diffs are merge conflicts. If the target and source branch contains a merge conflict, the branches -cannot be automatically merged. The [recording on -YouTube](https://www.youtube.com/watch?v=GFXIFA4ZuZw&feature=youtu.be&ab_channel=GitLabUnfiltered) +cannot be automatically merged. The +<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [recording on YouTube](https://www.youtube.com/watch?v=GFXIFA4ZuZw&feature=youtu.be&ab_channel=GitLabUnfiltered) is a quick introduction to the problem and the motivation behind the [epic](https://gitlab.com/groups/gitlab-org/-/epics/854). In 13.5 a solution for both-modified merge diff --git a/doc/development/elasticsearch.md b/doc/development/elasticsearch.md index 51497b51e9a..8604afaad04 100644 --- a/doc/development/elasticsearch.md +++ b/doc/development/elasticsearch.md @@ -13,9 +13,9 @@ the [Elasticsearch integration documentation](../integration/elasticsearch.md#en ## Deep Dive -In June 2019, Mario de la Ossa hosted a Deep Dive (GitLab team members only: `https://gitlab.com/gitlab-org/create-stage/issues/1`) on the GitLab [Elasticsearch integration](../integration/elasticsearch.md) to share his domain specific knowledge with anyone who may work in this part of the codebase in the future. You can find the [recording on YouTube](https://www.youtube.com/watch?v=vrvl-tN2EaA), and the slides on [Google Slides](https://docs.google.com/presentation/d/1H-pCzI_LNrgrL5pJAIQgvLX8Ji0-jIKOg1QeJQzChug/edit) and in [PDF](https://gitlab.com/gitlab-org/create-stage/uploads/c5aa32b6b07476fa8b597004899ec538/Elasticsearch_Deep_Dive.pdf). Everything covered in this deep dive was accurate as of GitLab 12.0, and while specific details may have changed since then, it should still serve as a good introduction. +In June 2019, Mario de la Ossa hosted a Deep Dive (GitLab team members only: `https://gitlab.com/gitlab-org/create-stage/issues/1`) on the GitLab [Elasticsearch integration](../integration/elasticsearch.md) to share his domain specific knowledge with anyone who may work in this part of the codebase in the future. You can find the <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [recording on YouTube](https://www.youtube.com/watch?v=vrvl-tN2EaA), and the slides on [Google Slides](https://docs.google.com/presentation/d/1H-pCzI_LNrgrL5pJAIQgvLX8Ji0-jIKOg1QeJQzChug/edit) and in [PDF](https://gitlab.com/gitlab-org/create-stage/uploads/c5aa32b6b07476fa8b597004899ec538/Elasticsearch_Deep_Dive.pdf). Everything covered in this deep dive was accurate as of GitLab 12.0, and while specific details may have changed since then, it should still serve as a good introduction. -In August 2020, a second Deep Dive was hosted, focusing on [GitLab-specific architecture for multi-indices support](#zero-downtime-reindexing-with-multiple-indices). The [recording on YouTube](https://www.youtube.com/watch?v=0WdPR9oB2fg) and the [slides](https://lulalala.gitlab.io/gitlab-elasticsearch-deepdive/) are available. Everything covered in this deep dive was accurate as of GitLab 13.3. +In August 2020, a second Deep Dive was hosted, focusing on [GitLab-specific architecture for multi-indices support](#zero-downtime-reindexing-with-multiple-indices). The <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [recording on YouTube](https://www.youtube.com/watch?v=0WdPR9oB2fg) and the [slides](https://lulalala.gitlab.io/gitlab-elasticsearch-deepdive/) are available. Everything covered in this deep dive was accurate as of GitLab 13.3. ## Supported Versions diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md index 7e581939346..40ed5b383b6 100644 --- a/doc/development/fe_guide/graphql.md +++ b/doc/development/fe_guide/graphql.md @@ -18,13 +18,13 @@ info: "See the Technical Writers assigned to Development Guidelines: https://abo **GraphQL at GitLab**: -- [🎬 GitLab Unfiltered GraphQL playlist](https://www.youtube.com/watch?v=wHPKZBDMfxE&list=PL05JrBw4t0KpcjeHjaRMB7IGB2oDWyJzv) -- [🎬 GraphQL at GitLab: Deep Dive](../api_graphql_styleguide.md#deep-dive) (video) by Nick Thomas +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [GitLab Unfiltered GraphQL playlist](https://www.youtube.com/watch?v=wHPKZBDMfxE&list=PL05JrBw4t0KpcjeHjaRMB7IGB2oDWyJzv) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [GraphQL at GitLab: Deep Dive](../api_graphql_styleguide.md#deep-dive) (video) by Nick Thomas - An overview of the history of GraphQL at GitLab (not frontend-specific) -- [🎬 GitLab Feature Walkthrough with GraphQL and Vue Apollo](https://www.youtube.com/watch?v=6yYp2zB7FrM) (video) by Natalia Tepluhina +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [GitLab Feature Walkthrough with GraphQL and Vue Apollo](https://www.youtube.com/watch?v=6yYp2zB7FrM) (video) by Natalia Tepluhina - A real-life example of implementing a frontend feature in GitLab using GraphQL -- [🎬 History of client-side GraphQL at GitLab](https://www.youtube.com/watch?v=mCKRJxvMnf0) (video) Illya Klymov and Natalia Tepluhina -- [🎬 From Vuex to Apollo](https://www.youtube.com/watch?v=9knwu87IfU8) (video) by Natalia Tepluhina +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [History of client-side GraphQL at GitLab](https://www.youtube.com/watch?v=mCKRJxvMnf0) (video) Illya Klymov and Natalia Tepluhina +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [From Vuex to Apollo](https://www.youtube.com/watch?v=9knwu87IfU8) (video) by Natalia Tepluhina - An overview of when Apollo might be a better choice than Vuex, and how one could go about the transition - [🛠Vuex -> Apollo Migration: a proof-of-concept project](https://gitlab.com/ntepluhina/vuex-to-apollo/blob/master/README.md) - A collection of examples that show the possible approaches for state management with Vue+GraphQL+(Vuex or Apollo) apps diff --git a/doc/development/feature_flags/index.md b/doc/development/feature_flags/index.md index e93a5b3de1b..4890cc5da35 100644 --- a/doc/development/feature_flags/index.md +++ b/doc/development/feature_flags/index.md @@ -31,7 +31,8 @@ In all cases, those working on the changes should ask themselves: > Why do I need to add a feature flag? If I don't add one, what options do I have to control the impact on application reliability, and user experience? -For perspective on why we limit our use of feature flags please see the following [video](https://www.youtube.com/watch?v=DQaGqyolOd8). +For perspective on why we limit our use of feature flags please see +<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Feature flags only when needed](https://www.youtube.com/watch?v=DQaGqyolOd8). In case you are uncertain if a feature flag is necessary, simply ask about this in an early merge request, and those reviewing the changes will likely provide you with an answer. diff --git a/doc/development/gitaly.md b/doc/development/gitaly.md index 5d062d7404e..e21257bc9e8 100644 --- a/doc/development/gitaly.md +++ b/doc/development/gitaly.md @@ -17,7 +17,7 @@ on the [Gitaly project](https://gitlab.com/gitlab-org/gitaly) and how to contrib Ruby developer, to share his domain specific knowledge with anyone who may work in this part of the codebase in the future. -You can find the [recording on YouTube](https://www.youtube.com/watch?v=BmlEWFS8ORo), and the slides +You can find the <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [recording on YouTube](https://www.youtube.com/watch?v=BmlEWFS8ORo), and the slides on [Google Slides](https://docs.google.com/presentation/d/1VgRbiYih9ODhcPnL8dS0W98EwFYpJ7GXMPpX-1TM6YE/edit) and in [PDF](https://gitlab.com/gitlab-org/create-stage/uploads/a4fdb1026278bda5c1c5bb574379cf80/Create_Deep_Dive__Gitaly_for_Create_Ruby_Devs.pdf). diff --git a/doc/development/integrations/secure_partner_integration.md b/doc/development/integrations/secure_partner_integration.md index 80f632639ca..b22ff529650 100644 --- a/doc/development/integrations/secure_partner_integration.md +++ b/doc/development/integrations/secure_partner_integration.md @@ -114,7 +114,7 @@ and complete an integration with the Secure stage. doing an [Unfiltered blog post](https://about.gitlab.com/handbook/marketing/blog/unfiltered/), doing a co-branded webinar, or producing a co-branded white paper. -We have a [video playlist](https://www.youtube.com/playlist?list=PL05JrBw4t0KpMqYxJiOLz-uBIr5w-yP4A) +We have a <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [video playlist](https://www.youtube.com/playlist?list=PL05JrBw4t0KpMqYxJiOLz-uBIr5w-yP4A) that may be helpful as part of this process. This covers various topics related to integrating your tool. diff --git a/doc/development/lfs.md b/doc/development/lfs.md index 9df1f659654..d102d50df27 100644 --- a/doc/development/lfs.md +++ b/doc/development/lfs.md @@ -11,7 +11,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w In April 2019, Francisco Javier López hosted a Deep Dive (GitLab team members only: `https://gitlab.com/gitlab-org/create-stage/issues/1`) on the GitLab [Git LFS](../topics/git/lfs/index.md) implementation to share his domain specific knowledge with anyone who may work in this part of the codebase in the future. -You can find the [recording on YouTube](https://www.youtube.com/watch?v=Yyxwcksr0Qc), +You can find the <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [recording on YouTube](https://www.youtube.com/watch?v=Yyxwcksr0Qc), and the slides on [Google Slides](https://docs.google.com/presentation/d/1E-aw6-z0rYd0346YhIWE7E9A65zISL9iIMAOq2zaw9E/edit) and in [PDF](https://gitlab.com/gitlab-org/create-stage/uploads/07a89257a140db067bdfb484aecd35e1/Git_LFS_Deep_Dive__Create_.pdf). Everything covered in this deep dive was accurate as of GitLab 11.10, and while specific diff --git a/doc/development/logging.md b/doc/development/logging.md index 9165abf9cdd..8147718d82a 100644 --- a/doc/development/logging.md +++ b/doc/development/logging.md @@ -291,7 +291,7 @@ method or variable shouldn't be evaluated right away) - Change `Gitlab::ApplicationContext` to accept these new values - Make sure the new attributes are accepted at [`Labkit::Context`](https://gitlab.com/gitlab-org/labkit-ruby/blob/master/lib/labkit/context.rb) -See our [HOWTO: Use Sidekiq metadata logs](https://www.youtube.com/watch?v=_wDllvO_IY0) for further knowledge on +See our <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [HOWTO: Use Sidekiq metadata logs](https://www.youtube.com/watch?v=_wDllvO_IY0) for further knowledge on creating visualizations in Kibana. The fields of the context are currently only logged for Sidekiq jobs triggered @@ -311,7 +311,7 @@ class MyExampleWorker def perform(*args) # Worker performs work # ... - + # The contents of value will appear in Kibana under `json.extra.my_example_worker.my_key` log_extra_metadata_on_done(:my_key, value) end diff --git a/doc/development/redis.md b/doc/development/redis.md index bb725e3c321..0dca415b77c 100644 --- a/doc/development/redis.md +++ b/doc/development/redis.md @@ -74,8 +74,10 @@ which is enabled for the `cache` and `shared_state` ## Redis in structured logging -For GitLab Team Members: There are [basic](https://www.youtube.com/watch?v=Uhdj19Dc6vU) and -[advanced](https://youtu.be/jw1Wv2IJxzs) videos that show how you can work with the Redis +For GitLab Team Members: There are <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> +[basic](https://www.youtube.com/watch?v=Uhdj19Dc6vU) and +<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [advanced](https://youtu.be/jw1Wv2IJxzs) +videos that show how you can work with the Redis structured logging fields on GitLab.com. Our [structured logging](logging.md#use-structured-json-logging) for web diff --git a/doc/development/refactoring_guide/index.md b/doc/development/refactoring_guide/index.md index 224b6bf9b38..a25000589c0 100644 --- a/doc/development/refactoring_guide/index.md +++ b/doc/development/refactoring_guide/index.md @@ -80,4 +80,4 @@ expect(cleanForSnapshot(wrapper.element)).toMatchSnapshot(); - [Pinning test in refactoring dropdown](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28173) - [Pinning test in refactoring vulnerability_details.vue](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25830/commits) - [Pinning test in refactoring notes_award_list.vue](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29528#pinning-test) -- [Video of pair programming session on pinning tests](https://youtu.be/LrakPcspBK4) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Video of pair programming session on pinning tests](https://youtu.be/LrakPcspBK4) diff --git a/doc/development/repository_mirroring.md b/doc/development/repository_mirroring.md index a47617c3d5b..61157c88618 100644 --- a/doc/development/repository_mirroring.md +++ b/doc/development/repository_mirroring.md @@ -13,7 +13,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w In December 2018, Tiago Botelho hosted a Deep Dive (GitLab team members only: `https://gitlab.com/gitlab-org/create-stage/issues/1`) on the GitLab [Pull Repository Mirroring functionality](../user/project/repository/repository_mirroring.md#pulling-from-a-remote-repository) to share his domain specific knowledge with anyone who may work in this part of the -codebase in the future. You can find the [recording on YouTube](https://www.youtube.com/watch?v=sSZq0fpdY-Y), +codebase in the future. You can find the <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [recording on YouTube](https://www.youtube.com/watch?v=sSZq0fpdY-Y), and the slides in [PDF](https://gitlab.com/gitlab-org/create-stage/uploads/8693404888a941fd851f8a8ecdec9675/Gitlab_Create_-_Pull_Mirroring_Deep_Dive.pdf). Everything covered in this deep dive was accurate as of GitLab 11.6, and while specific details may have changed since then, it should still serve as a good introduction. diff --git a/doc/development/secure_coding_guidelines.md b/doc/development/secure_coding_guidelines.md index 43b2a3e173b..a73f57e727a 100644 --- a/doc/development/secure_coding_guidelines.md +++ b/doc/development/secure_coding_guidelines.md @@ -288,9 +288,9 @@ XSS issues are commonly classified in three categories, by their delivery method The injected client-side code is executed on the victim's browser in the context of their current session. This means the attacker could perform any same action the victim would normally be able to do through a browser. The attacker would also have the ability to: -- [log victim keystrokes](https://youtu.be/2VFavqfDS6w?t=1367) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [log victim keystrokes](https://youtu.be/2VFavqfDS6w?t=1367) - launch a network scan from the victim's browser -- potentially [obtain the victim's session tokens](https://youtu.be/2VFavqfDS6w?t=739) +- potentially <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [obtain the victim's session tokens](https://youtu.be/2VFavqfDS6w?t=739) - perform actions that lead to data loss/theft or account takeover Much of the impact is contingent upon the function of the application and the capabilities of the victim's session. For further impact possibilities, please check out [the beef project](https://beefproject.com/). @@ -309,14 +309,14 @@ In most situations, a two-step solution can be used: input validation and output ##### Setting expectations -For any and all input fields, ensure to define expectations on the type/format of input, the contents, [size limits](https://youtu.be/2VFavqfDS6w?t=7582), the context in which it will be output. It's important to work with both security and product teams to determine what is considered acceptable input. +For any and all input fields, ensure to define expectations on the type/format of input, the contents, <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [size limits](https://youtu.be/2VFavqfDS6w?t=7582), the context in which it will be output. It's important to work with both security and product teams to determine what is considered acceptable input. ##### Validate input - Treat all user input as untrusted. - Based on the expectations you [defined above](#setting-expectations): - - Validate the [input size limits](https://youtu.be/2VFavqfDS6w?t=7582). - - Validate the input using an [allowlist approach](https://youtu.be/2VFavqfDS6w?t=7816) to only allow characters through which you are expecting to receive for the field. + - Validate the <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [input size limits](https://youtu.be/2VFavqfDS6w?t=7582). + - Validate the input using an <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [allowlist approach](https://youtu.be/2VFavqfDS6w?t=7816) to only allow characters through which you are expecting to receive for the field. - Input which fails validation should be **rejected**, and not sanitized. - When adding redirects or links to a user-controlled URL, ensure that the scheme is HTTP or HTTPS. Allowing other schemes like `javascript://` can lead to XSS and other security issues. @@ -328,8 +328,8 @@ Once you've [determined when and where](#setting-expectations) the user submitte - Content placed inside HTML elements need to be [HTML entity encoded](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#rule-1---html-escape-before-inserting-untrusted-data-into-html-element-content). - Content placed into a JSON response needs to be [JSON encoded](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#rule-31---html-escape-json-values-in-an-html-context-and-read-the-data-with-jsonparse). -- Content placed inside [HTML URL GET parameters](https://youtu.be/2VFavqfDS6w?t=3494) need to be [URL-encoded](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#rule-5---url-escape-before-inserting-untrusted-data-into-html-url-parameter-values) -- [Additional contexts may require context-specific encoding](https://youtu.be/2VFavqfDS6w?t=2341). +- Content placed inside <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [HTML URL GET parameters](https://youtu.be/2VFavqfDS6w?t=3494) need to be [URL-encoded](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#rule-5---url-escape-before-inserting-untrusted-data-into-html-url-parameter-values) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Additional contexts may require context-specific encoding](https://youtu.be/2VFavqfDS6w?t=2341). ### Additional information @@ -352,10 +352,10 @@ Do also sanitize and validate URL schemes. References: -- [XSS Defense in Rails](https://youtu.be/2VFavqfDS6w?t=2442) -- [XSS Defense with HAML](https://youtu.be/2VFavqfDS6w?t=2796) -- [Validating Untrusted URLs in Ruby](https://youtu.be/2VFavqfDS6w?t=3936) -- [RoR Model Validators](https://youtu.be/2VFavqfDS6w?t=7636) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [XSS Defense in Rails](https://youtu.be/2VFavqfDS6w?t=2442) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [XSS Defense with HAML](https://youtu.be/2VFavqfDS6w?t=2796) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Validating Untrusted URLs in Ruby](https://youtu.be/2VFavqfDS6w?t=3936) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [RoR Model Validators](https://youtu.be/2VFavqfDS6w?t=7636) #### XSS mitigation and prevention in JavaScript and Vue @@ -376,7 +376,7 @@ References: #### Content Security Policy -- [Content Security Policy](https://www.youtube.com/watch?v=2VFavqfDS6w&t=12991s) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Content Security Policy](https://www.youtube.com/watch?v=2VFavqfDS6w&t=12991s) - [Use nonce-based Content Security Policy for inline JavaScript](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/65330) #### Free form input field @@ -390,26 +390,26 @@ References: ### Internal Developer Training -- [Introduction to XSS](https://www.youtube.com/watch?v=PXR8PTojHmc&t=7785s) -- [Reflected XSS](https://youtu.be/2VFavqfDS6w?t=603s) -- [Persistent XSS](https://youtu.be/2VFavqfDS6w?t=643) -- [DOM XSS](https://youtu.be/2VFavqfDS6w?t=5871) -- [XSS in depth](https://www.youtube.com/watch?v=2VFavqfDS6w&t=111s) -- [XSS Defense](https://youtu.be/2VFavqfDS6w?t=1685) -- [XSS Defense in Rails](https://youtu.be/2VFavqfDS6w?t=2442) -- [XSS Defense with HAML](https://youtu.be/2VFavqfDS6w?t=2796) -- [JavaScript URLs](https://youtu.be/2VFavqfDS6w?t=3274) -- [URL encoding context](https://youtu.be/2VFavqfDS6w?t=3494) -- [Validating Untrusted URLs in Ruby](https://youtu.be/2VFavqfDS6w?t=3936) -- [HTML Sanitization](https://youtu.be/2VFavqfDS6w?t=5075) -- [DOMPurify](https://youtu.be/2VFavqfDS6w?t=5381) -- [Safe Client-side JSON Handling](https://youtu.be/2VFavqfDS6w?t=6334) -- [iframe sandboxing](https://youtu.be/2VFavqfDS6w?t=7043) -- [Input Validation](https://youtu.be/2VFavqfDS6w?t=7489) -- [Validate size limits](https://youtu.be/2VFavqfDS6w?t=7582) -- [RoR model validators](https://youtu.be/2VFavqfDS6w?t=7636) -- [Allowlist input validation](https://youtu.be/2VFavqfDS6w?t=7816) -- [Content Security Policy](https://www.youtube.com/watch?v=2VFavqfDS6w&t=12991s) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Introduction to XSS](https://www.youtube.com/watch?v=PXR8PTojHmc&t=7785s) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Reflected XSS](https://youtu.be/2VFavqfDS6w?t=603s) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Persistent XSS](https://youtu.be/2VFavqfDS6w?t=643) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [DOM XSS](https://youtu.be/2VFavqfDS6w?t=5871) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [XSS in depth](https://www.youtube.com/watch?v=2VFavqfDS6w&t=111s) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [XSS Defense](https://youtu.be/2VFavqfDS6w?t=1685) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [XSS Defense in Rails](https://youtu.be/2VFavqfDS6w?t=2442) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [XSS Defense with HAML](https://youtu.be/2VFavqfDS6w?t=2796) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [JavaScript URLs](https://youtu.be/2VFavqfDS6w?t=3274) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [URL encoding context](https://youtu.be/2VFavqfDS6w?t=3494) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Validating Untrusted URLs in Ruby](https://youtu.be/2VFavqfDS6w?t=3936) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [HTML Sanitization](https://youtu.be/2VFavqfDS6w?t=5075) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [DOMPurify](https://youtu.be/2VFavqfDS6w?t=5381) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Safe Client-side JSON Handling](https://youtu.be/2VFavqfDS6w?t=6334) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [iframe sandboxing](https://youtu.be/2VFavqfDS6w?t=7043) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Input Validation](https://youtu.be/2VFavqfDS6w?t=7489) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Validate size limits](https://youtu.be/2VFavqfDS6w?t=7582) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [RoR model validators](https://youtu.be/2VFavqfDS6w?t=7636) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Allowlist input validation](https://youtu.be/2VFavqfDS6w?t=7816) +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Content Security Policy](https://www.youtube.com/watch?v=2VFavqfDS6w&t=12991s) ## Path Traversal guidelines diff --git a/doc/development/snowplow.md b/doc/development/snowplow.md index cee093ac63a..b0f4577d8b1 100644 --- a/doc/development/snowplow.md +++ b/doc/development/snowplow.md @@ -382,7 +382,7 @@ Snowplow Micro is a Docker-based solution for testing frontend and backend event - Read [Introducing Snowplow Micro](https://snowplowanalytics.com/blog/2019/07/17/introducing-snowplow-micro/) - Look at the [Snowplow Micro repository](https://github.com/snowplow-incubator/snowplow-micro) -- Watch our [installation guide recording](https://www.youtube.com/watch?v=OX46fo_A0Ag) +- Watch our <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [installation guide recording](https://www.youtube.com/watch?v=OX46fo_A0Ag) 1. Ensure Docker is installed and running. diff --git a/doc/integration/google_workspace_saml.md b/doc/integration/google_workspace_saml.md new file mode 100644 index 00000000000..7b561750b0f --- /dev/null +++ b/doc/integration/google_workspace_saml.md @@ -0,0 +1,163 @@ +--- +type: reference +stage: Manage +group: Access +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# Google Workspace SSO provider + +Google Workspace (formerly G Suite) is a [Single Sign-on provider](https://support.google.com/a/answer/60224?hl=en) that can be used to authenticate +with GitLab. + +The following documentation enables Google Workspace as a SAML provider for GitLab. + +## Configure the Google Workspace SAML app + +The following guidance is based on this Google Workspace article, on how to [Set up your own custom SAML application](https://support.google.com/a/answer/6087519?hl=en): + +Make sure you have access to a Google Workspace [Super Admin](https://support.google.com/a/answer/2405986#super_admin) account. + Follow the instructions in the linked Google Workspace article, where you'll need the following information: + +| | Typical value | Description | +|------------------|--------------------------------------------------|----------------------------------------------------------| +| Name of SAML App | GitLab | Other names OK. | +| ACS URL | `https://<GITLAB_DOMAIN>/users/auth/saml/callback` | ACS is short for Assertion Consumer Service. | +| GITLAB_DOMAIN | `gitlab.example.com` | Set to the domain of your GitLab instance. | +| Entity ID | `https://gitlab.example.com` | A value unique to your SAML app, you'll set it to the `issuer` in your GitLab configuration. | +| Name ID format | EMAIL | Required value. Also known as `name_identifier_format` | +| Name ID | Primary email address | Make sure someone receives content sent to that address | +| First name | `first_name` | Required value to communicate with GitLab. | +| Last name | `last_name` | Required value to communicate with GitLab. | + +You will also need to setup the following SAML attribute mappings: + +| Google Directory attributes | App attributes | +|-----------------------------------|----------------| +| Basic information > Email | `email` | +| Basic Information > First name | `first_name` | +| Basic Information > Last name | `last_name` | + +You may also use some of this information when you [Configure GitLab](#configure-gitlab). + +When configuring the Google Workspace SAML app, be sure to record the following information: + +| | Value | Description | +|-------------|--------------|-----------------------------------------------------------------------------------| +| SSO URL | Depends | Google Identity Provider details. Set to the GitLab `idp_sso_target_url` setting. | +| Certificate | Downloadable | Run `openssl x509 -in <your_certificate.crt> -noout -fingerprint` to generate the SHA1 fingerprint that can be used in the `idp_cert_fingerprint` setting. | + +While the Google Workspace Admin provides IDP metadata, Entity ID and SHA-256 fingerprint, +GitLab does not need that information to connect to the Google Workspace SAML app. + +--- + +Now that the Google Workspace SAML app is configured, it's time to enable it in GitLab. + +## Configure GitLab + +1. See [Initial OmniAuth Configuration](../integration/omniauth.md#initial-omniauth-configuration) + for initial settings. + +1. To allow people to register for GitLab, through their Google accounts, add the following + values to your configuration: + + **For Omnibus GitLab installations** + + Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + gitlab_rails['omniauth_allow_single_sign_on'] = ['saml'] + gitlab_rails['omniauth_block_auto_created_users'] = false + ``` + + --- + + **For installations from source** + + Edit `config/gitlab.yml`: + + ```yaml + allow_single_sign_on: ["saml"] + block_auto_created_users: false + ``` + +1. If an existing GitLab user has the same email address as a Google Workspace user, the registration + process automatically links their accounts, if you add the following setting: + their email addresses match by adding the following setting: + + **For Omnibus GitLab installations** + + Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + gitlab_rails['omniauth_auto_link_saml_user'] = true + ``` + + --- + + **For installations from source** + + Edit `config/gitlab.yml`: + + ```yaml + auto_link_saml_user: true + ``` + +1. Add the provider configuration. + +For guidance on how to set up these values, see the [SAML General Setup steps](saml.md#general-setup). +Pay particular attention to the values for: + +- `assertion_consumer_service_url` +- `idp_cert_fingerprint` +- `idp_sso_target_url` +- `issuer` +- `name_identifier_format` + + **For Omnibus GitLab installations** + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + name: 'saml', + args: { + assertion_consumer_service_url: 'https://<GITLAB_DOMAIN>/users/auth/saml/callback', + idp_cert_fingerprint: '00:00:00:00:00:00:0:00:00:00:00:00:00:00:00:00', + idp_sso_target_url: 'https://accounts.google.com/o/saml2/idp?idpid=00000000', + issuer: 'https://<GITLAB_DOMAIN>', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress' + }, + label: 'Google Workspace' # optional label for SAML log in button, defaults to "Saml" + } + ] + ``` + + **For installations from source** + + ```yaml + - { + name: 'saml', + args: { + assertion_consumer_service_url: 'https://<GITLAB_DOMAIN>/users/auth/saml/callback', + idp_cert_fingerprint: '00:00:00:00:00:00:0:00:00:00:00:00:00:00:00:00', + idp_sso_target_url: 'https://accounts.google.com/o/saml2/idp?idpid=00000000', + issuer: 'https://<GITLAB_DOMAIN>', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress' + }, + label: 'Google Workspace' # optional label for SAML log in button, defaults to "Saml" + } + ``` + +1. [Reconfigure](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure) or [restart](../administration/restart_gitlab.md#installations-from-source) GitLab for Omnibus and installations + from source respectively for the changes to take effect. + +To avoid caching issues, test the result on an incognito or private browser window. + +## Troubleshooting + +The Google Workspace documentation on [SAML app error messages](https://support.google.com/a/answer/6301076?hl=en) is helpful for debugging if you are seeing an error from Google while signing in. +Pay particular attention to the following 403 errors: + +- `app_not_configured` +- `app_not_configured_for_user` diff --git a/doc/subscriptions/bronze_starter.md b/doc/subscriptions/bronze_starter.md new file mode 100644 index 00000000000..547dd6ea385 --- /dev/null +++ b/doc/subscriptions/bronze_starter.md @@ -0,0 +1,135 @@ +--- +stage: Fulfillment +group: Purchase +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# Features available to Starter and Bronze subscribers + +Although GitLab has discontinued selling the Bronze and Starter tiers, GitLab +continues to honor the entitlements of existing Bronze and Starter tier GitLab +customers for the duration of their contracts at that level. + +These features remain available to Bronze and Starter customers, even though +the tiers are no longer mentioned in GitLab documentation: + +- [Activate GitLab EE with a license](../user/admin_area/license.md) +- [Adding a help message to the login page](../user/admin_area/settings/help_page.md#adding-a-help-message-to-the-login-page) +- [Burndown and burnup charts](../user/project/milestones/burndown_and_burnup_charts.md), + including [per-project charts](../user/project/milestones/index.md#project-burndown-charts) and + [per-group charts](../user/project/milestones/index.md#group-burndown-charts) +- [Code owners](../user/project/code_owners.md) +- Description templates: + - [Setting a default template for merge requests and issues](../user/project/description_templates.md#setting-a-default-template-for-merge-requests-and-issues) +- [Email from GitLab](../tools/email.md) +- Groups: + - [Creating group memberships via CN](../user/group/index.md#creating-group-links-via-cn) + - [Group push rules](../user/group/index.md#group-push-rules) + - [Managing group memberships via LDAP](../user/group/index.md#manage-group-memberships-via-ldap) + - [Member locking](../user/group/index.md#member-lock) + - [Overriding user permissions](../user/group/index.md#overriding-user-permissions) + - [User contribution analysis](../user/group/index.md#user-contribution-analysis) + - [Kerberos integration](../integration/kerberos.md) +- Issue Boards: + - [Configurable issue boards](../user/project/issue_board.md#configurable-issue-boards) + - [Sum of issue weights](../user/project/issue_board.md#sum-of-issue-weights) + - [Work In Progress limits](../user/project/issue_board.md#work-in-progress-limits) +- Issues: + - [Multiple assignees for issues](../user/project/issues/multiple_assignees_for_issues.md) + - [Issue weights](../user/project/issues/issue_weight.md) + - [Issue histories](../user/project/issues/issue_data_and_actions.md#issue-history) contain changes to issue description + - [Adding an issue to an iteration](../user/project/issues/managing_issues.md#add-an-issue-to-an-iteration) +- [Iterations](../user/group/iterations/index.md) +- [Kerberos integration](../integration/kerberos.md) +- LDAP: + - Querying LDAP [from the Rails console](../administration/auth/ldap/ldap-troubleshooting.md#query-ldap), or + [querying a single group](../administration/auth/ldap/ldap-troubleshooting.md#query-a-group-in-ldap) + - [Sync all users](../administration/auth/ldap/ldap-troubleshooting.md#sync-all-users) + - [Group management through LDAP](../administration/auth/ldap/ldap-troubleshooting.md#group-memberships) + - Syncing information through LDAP: + - Groups: [one group](../administration/auth/ldap/ldap-troubleshooting.md#sync-one-group), + [all groups programmatically](../administration/auth/ldap/index.md#group-sync), + [group sync schedule](../administration/auth/ldap/index.md#adjusting-ldap-group-sync-schedule), and + [all groups manually](../administration/auth/ldap/ldap-troubleshooting.md#sync-all-groups) + - [Configuration settings](../administration/auth/ldap/index.md#ldap-sync-configuration-settings) + - Users: [all users](../administration/auth/ldap/index.md#user-sync), + [administrators](../administration/auth/ldap/index.md#administrator-sync), + [user sync schedule](../administration/auth/ldap/index.md#adjusting-ldap-user-sync-schedule) + - [Adding group links](../administration/auth/ldap/index.md#adding-group-links) + - [Lock memberships to LDAP synchronization](../administration/auth/ldap/index.md#global-group-memberships-lock) + - Rake tasks for [LDAP tasks](../administration/raketasks/ldap.md), including + [syncing groups](../administration/raketasks/ldap.md#run-a-group-sync) +- Logging: + - [`audit_json.log`](../administration/logs.md#audit_jsonlog) (specific entries) + - [`elasticsearch.log`](../administration/logs.md#elasticsearchlog) +- Merge requests: + - [Merge request approvals](../user/project/merge_requests/merge_request_approvals.md) + - [Multiple assignees](../user/project/merge_requests/getting_started.md#multiple-assignees) + - [Approval Rule information for Reviewers](../user/project/merge_requests/getting_started.md#approval-rule-information-for-reviewers), and [enabling or disabling it](../user/project/merge_requests/getting_started.md#enable-or-disable-approval-rule-information-for-reviewers) + - [Required Approvals](../user/project/merge_requests/merge_request_approvals.md#required-approvals) + - [Code Owners as eligible approvers](../user/project/merge_requests/merge_request_approvals.md#code-owners-as-eligible-approvers) + - All [Approval rules](../user/project/merge_requests/merge_request_approvals.md#approval-rules) features + - [Restricting push and merge access to certain users](../user/project/protected_branches.md#restricting-push-and-merge-access-to-certain-users) +- Metrics and analytics: + - [Contribution Analytics](../user/group/contribution_analytics/index.md) + - [Merge Request Analytics](../user/analytics/merge_request_analytics.md) + - [Code Review Analytics](../user/analytics/code_review_analytics.md) + - [Audit Events](../administration/audit_events.md), including + [Group events](../administration/audit_events.md#group-events) and + [Project events](../administration/audit_events.md#project-events) +- Rake tasks: + - [Displaying GitLab license information](../administration/raketasks/maintenance.md#show-gitlab-license-information) +- Reference Architecture information: + - [Traffic load balancers](../administration/reference_architectures/index.md#traffic-load-balancer) + - [Zero downtime updates](../administration/reference_architectures/index.md#zero-downtime-updates) +- Repositories: + - [Repository size limit](../user/admin_area/settings/account_and_limit_settings.md#repository-size-limit) + - Repository mirroring: + - [Pull mirroring](../user/project/repository/repository_mirroring.md#pulling-from-a-remote-repository) outside repositories in a GitLab repository + - [Overwrite diverged branches](../user/project/repository/repository_mirroring.md#overwrite-diverged-branches) + - [Trigger pipelines for mirror updates](../user/project/repository/repository_mirroring.md#trigger-pipelines-for-mirror-updates) + - [Hard failures](../user/project/repository/repository_mirroring.md#hard-failure) when mirroring fails + - [Trigger pull mirroring from the API](../user/project/repository/repository_mirroring.md#trigger-an-update-using-the-api) + - [Mirror only protected branches](../user/project/repository/repository_mirroring.md#mirror-only-protected-branches) + - [Bidirectional mirroring](../user/project/repository/repository_mirroring.md#bidirectional-mirroring) + - [Mirroring with Perforce Helix via Git Fusion](../user/project/repository/repository_mirroring.md#mirroring-with-perforce-helix-via-git-fusion) +- Runners: + - Run pipelines in the parent project [for merge requests from a forked project](../ci/merge_request_pipelines/index.md#run-pipelines-in-the-parent-project-for-merge-requests-from-a-forked-project) + - [Shared runners pipeline minutes quota](../user/admin_area/settings/continuous_integration.md#shared-runners-pipeline-minutes-quota) +- [Push rules](../push_rules/push_rules.md) +- SAML for self-managed GitLab instance: + - [Administrator groups](../integration/saml.md#admin-groups) + - [Auditor groups](../integration/saml.md#auditor-groups) + - [External groups](../integration/saml.md#external-groups) + - [Required groups](../integration/saml.md#required-groups) +- Search: + - [Filtering merge requests by approvers](../user/search/index.md#filtering-merge-requests-by-approvers) + - [Filtering merge requests by "approved by"](../user/search/index.md#filtering-merge-requests-by-approved-by) + - [Advanced Global Search (Elasticsearch)](../user/search/advanced_global_search.md) + - [Advanced Search Syntax](../user/search/advanced_search_syntax.md) +- [Service Desk](../user/project/service_desk.md) + +The following developer features continue to be available to Starter and +Bronze-level subscribers: + +- APIs: + - LDAP synchronization: + - Certain fields in the [group details API](../api/groups.md#details-of-a-group) + - [syncing groups](../api/groups.md#sync-group-with-ldap) + - Listing, adding, and deleting [group links](../api/groups.md#list-ldap-group-links) + - [Push rules](../api/groups.md#push-rules) + - [Audit events](../api/audit_events.md), including + [group audit events](../api/groups.md#group-audit-events) and + [project audit events](../api/audit_events.md#project-audit-events) + - Projects API: certain fields in the [Create project API](../api/projects.md) + - [Resource iteration events API](../api/resource_iteration_events.md) + - Group milestones API: [Get all burndown chart events for a single milestone](../api/group_milestones.md#get-all-burndown-chart-events-for-a-single-milestone) + - [Group iterations API](../api/group_iterations.md) + - Project milestones API: [Get all burndown chart events for a single milestone](../api/milestones.md#get-all-burndown-chart-events-for-a-single-milestone) + - [Project iterations API](../api/iterations.md) + - Fields in the [Search API](../api/search.md) available only to [Advanced Global Search (Elasticsearch)](../integration/elasticsearch.md) users + - Fields in the [Merge requests API](../api/merge_requests.md) for [merge request approvals](../user/project/merge_requests/merge_request_approvals.md) + - Fields in the [Protected branches API](../api/protected_branches.md) that specify users or groups allowed to merge + - [Merge request approvals API](../api/merge_request_approvals.md) +- Development information: + - [Run Jenkins in a macOS development environment](../development/integrations/jenkins.md) diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 823e4b86d0f..2f5c2888469 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -706,6 +706,9 @@ To enable this feature: This will enable the domain-checking for all new users added to the group from this moment on. +NOTE: +Domain restrictions only apply to groups and do not prevent users from being added as members of projects owned by the restricted group. + #### Group file templates **(PREMIUM)** Group file templates allow you to share a set of templates for common file diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 858e11aca50..43e867472bd 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -253,15 +253,15 @@ If you're new to this, don't be :fearful:. You can join the emoji :family:. All Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup: ``` -Sometimes you want to <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/monkey.png" width="20px" height="20px" style="display:inline;margin:0"> around a bit and add some <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/star2.png" width="20px" height="20px" style="display:inline;margin:0"> to your <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/speech_balloon.png" width="20px" height="20px" style="display:inline;margin:0">. Well we have a gift for you: +Sometimes you want to <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/monkey.png" width="20px" height="20px" style="display:inline;margin:0;border: 0"> around a bit and add some <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/star2.png" width="20px" height="20px" style="display:inline;margin:0;border: 0"> to your <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/speech_balloon.png" width="20px" height="20px" style="display:inline;margin:0;border: 0">. Well we have a gift for you: -<img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/zap.png" width="20px" height="20px" style="display:inline;margin:0">You can use emoji anywhere GFM is supported. <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/v.png" width="20px" height="20px" style="display:inline;margin:0"> +<img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/zap.png" width="20px" height="20px" style="display:inline;margin:0;border: 0">You can use emoji anywhere GFM is supported. <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/v.png" width="20px" height="20px" style="display:inline;margin:0;border: 0"> -You can use it to point out a <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/bug.png" width="20px" height="20px" style="display:inline;margin:0"> or warn about <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/speak_no_evil.png" width="20px" height="20px" style="display:inline;margin:0"> patches. And if someone improves your really <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/snail.png" width="20px" height="20px" style="display:inline;margin:0"> code, send them some <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/birthday.png" width="20px" height="20px" style="display:inline;margin:0">. People will <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/heart.png" width="20px" height="20px" style="display:inline;margin:0"> you for that. +You can use it to point out a <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/bug.png" width="20px" height="20px" style="display:inline;margin:0;border: 0"> or warn about <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/speak_no_evil.png" width="20px" height="20px" style="display:inline;margin:0;border: 0"> patches. And if someone improves your really <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/snail.png" width="20px" height="20px" style="display:inline;margin:0;border: 0"> code, send them some <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/birthday.png" width="20px" height="20px" style="display:inline;margin:0;border: 0">. People will <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/heart.png" width="20px" height="20px" style="display:inline;margin:0;border: 0"> you for that. -If you're new to this, don't be <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/fearful.png" width="20px" height="20px" style="display:inline;margin:0">. You can join the emoji <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/family.png" width="20px" height="20px" style="display:inline;margin:0">. All you need to do is to look up one of the supported codes. +If you're new to this, don't be <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/fearful.png" width="20px" height="20px" style="display:inline;margin:0;border: 0">. You can join the emoji <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/family.png" width="20px" height="20px" style="display:inline;margin:0;border: 0">. All you need to do is to look up one of the supported codes. -Consult the [Emoji Cheat Sheet](https://www.webfx.com/tools/emoji-cheat-sheet/) for a list of all supported emoji codes. <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/thumbsup.png" width="20px" height="20px" style="display:inline;margin:0"> +Consult the [Emoji Cheat Sheet](https://www.webfx.com/tools/emoji-cheat-sheet/) for a list of all supported emoji codes. <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/assets/images/emoji/thumbsup.png" width="20px" height="20px" style="display:inline;margin:0;border: 0"> #### Emoji and your OS diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb index 6798c4d284b..71a18524104 100644 --- a/lib/api/helpers/notes_helpers.rb +++ b/lib/api/helpers/notes_helpers.rb @@ -138,7 +138,7 @@ module API parent = noteable_parent(noteable) ::Discussions::ResolveService.new(parent, current_user, one_or_more_discussions: discussion).execute else - discussion.unresolve! + ::Discussions::UnresolveService.new(discussion, current_user).execute end present discussion, with: Entities::Discussion diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index f935c677930..ee11c39b6ec 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -23,6 +23,7 @@ module Gitlab deployment_minimum_id deployment_maximum_id auth_providers + recorded_at ).freeze class << self @@ -75,7 +76,7 @@ module Gitlab end def recorded_at - Time.current + @recorded_at ||= Time.current end # rubocop: disable Metrics/AbcSize diff --git a/lib/gitlab/usage_data_counters/known_events/common.yml b/lib/gitlab/usage_data_counters/known_events/common.yml index 7eab1818392..532ec6fda55 100644 --- a/lib/gitlab/usage_data_counters/known_events/common.yml +++ b/lib/gitlab/usage_data_counters/known_events/common.yml @@ -476,6 +476,16 @@ category: code_review aggregation: weekly feature_flag: usage_data_i_code_review_user_reopen_mr +- name: i_code_review_user_resolve_thread + redis_slot: code_review + category: code_review + aggregation: weekly + feature_flag: usage_data_i_code_review_user_resolve_thread +- name: i_code_review_user_unresolve_thread + redis_slot: code_review + category: code_review + aggregation: weekly + feature_flag: usage_data_i_code_review_user_unresolve_thread - name: i_code_review_user_merge_mr redis_slot: code_review category: code_review diff --git a/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb b/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb index 78b04a89aed..4facabb56c0 100644 --- a/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb +++ b/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb @@ -20,6 +20,8 @@ module Gitlab MR_REMOVE_MULTILINE_COMMENT_ACTION = 'i_code_review_user_remove_multiline_mr_comment' MR_ADD_SUGGESTION_ACTION = 'i_code_review_user_add_suggestion' MR_APPLY_SUGGESTION_ACTION = 'i_code_review_user_apply_suggestion' + MR_RESOLVE_THREAD_ACTION = 'i_code_review_user_resolve_thread' + MR_UNRESOLVE_THREAD_ACTION = 'i_code_review_user_unresolve_thread' class << self def track_mr_diffs_action(merge_request:) @@ -47,6 +49,14 @@ module Gitlab track_unique_action_by_user(MR_REOPEN_ACTION, user) end + def track_resolve_thread_action(user:) + track_unique_action_by_user(MR_RESOLVE_THREAD_ACTION, user) + end + + def track_unresolve_thread_action(user:) + track_unique_action_by_user(MR_UNRESOLVE_THREAD_ACTION, user) + end + def track_create_comment_action(note:) track_unique_action_by_user(MR_CREATE_COMMENT_ACTION, note.author) track_multiline_unique_action(MR_CREATE_MULTILINE_COMMENT_ACTION, note) diff --git a/lib/gitlab/utils/usage_data.rb b/lib/gitlab/utils/usage_data.rb index baccadd9594..64be5c54f0f 100644 --- a/lib/gitlab/utils/usage_data.rb +++ b/lib/gitlab/utils/usage_data.rb @@ -39,6 +39,9 @@ module Gitlab FALLBACK = -1 DISTRIBUTED_HLL_FALLBACK = -2 + ALL_TIME_PERIOD_HUMAN_NAME = "all_time" + WEEKLY_PERIOD_HUMAN_NAME = "weekly" + MONTHLY_PERIOD_HUMAN_NAME = "monthly" def count(relation, column = nil, batch: true, batch_size: nil, start: nil, finish: nil) if batch @@ -61,10 +64,13 @@ module Gitlab end def estimate_batch_distinct_count(relation, column = nil, batch_size: nil, start: nil, finish: nil) - Gitlab::Database::PostgresHll::BatchDistinctCounter + buckets = Gitlab::Database::PostgresHll::BatchDistinctCounter .new(relation, column) .execute(batch_size: batch_size, start: start, finish: finish) - .estimated_distinct_count + + yield buckets if block_given? + + buckets.estimated_distinct_count rescue ActiveRecord::StatementInvalid FALLBACK # catch all rescue should be removed as a part of feature flag rollout issue @@ -74,6 +80,27 @@ module Gitlab DISTRIBUTED_HLL_FALLBACK end + def save_aggregated_metrics(metric_name:, time_period:, recorded_at_timestamp:, data:) + unless data.is_a? ::Gitlab::Database::PostgresHll::Buckets + Gitlab::ErrorTracking.track_and_raise_for_dev_exception(StandardError.new("Unsupported data type: #{data.class}")) + return + end + + # the longest recorded usage ping generation time for gitlab.com + # was below 40 hours, there is added error margin of 20 h + usage_ping_generation_period = 80.hours + + # add timestamp at the end of the key to avoid stale keys if + # usage ping job is retried + redis_key = "#{metric_name}_#{time_period_to_human_name(time_period)}-#{recorded_at_timestamp}" + + Gitlab::Redis::SharedState.with do |redis| + redis.set(redis_key, data.to_json, ex: usage_ping_generation_period) + end + rescue ::Redis::CommandError => e + Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e) + end + def sum(relation, column, batch_size: nil, start: nil, finish: nil) Gitlab::Database::BatchCount.batch_sum(relation, column, batch_size: batch_size, start: start, finish: finish) rescue ActiveRecord::StatementInvalid @@ -125,6 +152,20 @@ module Gitlab Gitlab::UsageDataCounters::HLLRedisCounter.track_event(event_name.to_s, values: values) end + def time_period_to_human_name(time_period) + return ALL_TIME_PERIOD_HUMAN_NAME if time_period.blank? + + date_range = time_period.values[0] + start_date = date_range.first.to_date + end_date = date_range.last.to_date + + if (end_date - start_date).to_i > 7 + MONTHLY_PERIOD_HUMAN_NAME + else + WEEKLY_PERIOD_HUMAN_NAME + end + end + private def prometheus_client(verify:) diff --git a/package.json b/package.json index 11508f12cd7..ab9bf515fdd 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@babel/plugin-proposal-private-methods": "^7.10.1", "@babel/plugin-syntax-import-meta": "^7.10.1", "@babel/preset-env": "^7.10.1", - "@gitlab/at.js": "1.5.5", + "@gitlab/at.js": "1.5.7", "@gitlab/favicon-overlay": "2.0.0", "@gitlab/svgs": "1.179.0", "@gitlab/tributejs": "1.0.0", diff --git a/spec/controllers/projects/discussions_controller_spec.rb b/spec/controllers/projects/discussions_controller_spec.rb index f9d16e761cb..8a793e29bfa 100644 --- a/spec/controllers/projects/discussions_controller_spec.rb +++ b/spec/controllers/projects/discussions_controller_spec.rb @@ -186,6 +186,13 @@ RSpec.describe Projects::DiscussionsController do expect(Note.find(note.id).discussion.resolved?).to be false end + it "tracks thread unresolve usage data" do + expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) + .to receive(:track_unresolve_thread_action).with(user: user) + + delete :unresolve, params: request_params + end + it "returns status 200" do delete :unresolve, params: request_params diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index d3bdf1baaae..52ad7efff12 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -63,53 +63,20 @@ RSpec.describe Projects::IssuesController do end end - describe 'the null hypothesis experiment', :snowplow do - it 'defines the expected before actions' do - expect(controller).to use_before_action(:run_null_hypothesis_experiment) - end - - context 'when rolled out to 100%' do - it 'assigns the candidate experience and tracks the event' do - get :index, params: { namespace_id: project.namespace, project_id: project } - - expect_snowplow_event( - category: 'null_hypothesis', - action: 'index', - context: [{ - schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-3-0', - data: { variant: 'candidate', experiment: 'null_hypothesis', key: anything } - }] - ) - end + describe 'the null hypothesis experiment', :experiment do + before do + stub_experiments(null_hypothesis: :candidate) end - context 'when not rolled out' do - before do - stub_feature_flags(null_hypothesis: false) - end - - it 'assigns the control experience and tracks the event' do - get :index, params: { namespace_id: project.namespace, project_id: project } - - expect_snowplow_event( - category: 'null_hypothesis', - action: 'index', - context: [{ - schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-3-0', - data: { variant: 'control', experiment: 'null_hypothesis', key: anything } - }] - ) - end + it 'defines the expected before actions' do + expect(controller).to use_before_action(:run_null_hypothesis_experiment) end - context 'when gitlab_experiments is disabled' do - it 'does not run the experiment at all' do - stub_feature_flags(gitlab_experiments: false) + it 'assigns the candidate experience and tracks the event' do + expect(experiment(:null_hypothesis)).to track('index').on_any_instance.for(:candidate) + .with_context(project: project) - expect(controller).not_to receive(:run_null_hypothesis_experiment) - - get :index, params: { namespace_id: project.namespace, project_id: project } - end + get :index, params: { namespace_id: project.namespace, project_id: project } end end end diff --git a/spec/experiments/application_experiment_spec.rb b/spec/experiments/application_experiment_spec.rb index ece52d37351..beefc09e591 100644 --- a/spec/experiments/application_experiment_spec.rb +++ b/spec/experiments/application_experiment_spec.rb @@ -2,9 +2,45 @@ require 'spec_helper' -RSpec.describe ApplicationExperiment do +RSpec.describe ApplicationExperiment, :experiment do subject { described_class.new(:stub) } + before do + allow(subject).to receive(:enabled?).and_return(true) + end + + describe "enabled" do + before do + allow(subject).to receive(:enabled?).and_call_original + + allow(Feature::Definition).to receive(:get).and_return('_instance_') + allow(Gitlab).to receive(:dev_env_or_com?).and_return(true) + allow(Feature).to receive(:get).and_return(double(state: :on)) + end + + it "is enabled when all criteria are met" do + expect(subject).to be_enabled + end + + it "isn't enabled if the feature definition doesn't exist" do + expect(Feature::Definition).to receive(:get).with('stub').and_return(nil) + + expect(subject).not_to be_enabled + end + + it "isn't enabled if we're not in dev or dotcom environments" do + expect(Gitlab).to receive(:dev_env_or_com?).and_return(false) + + expect(subject).not_to be_enabled + end + + it "isn't enabled if the feature flag state is :off" do + expect(Feature).to receive(:get).with('stub').and_return(double(state: :off)) + + expect(subject).not_to be_enabled + end + end + describe "publishing results" do it "tracks the assignment" do expect(subject).to receive(:track).with(:assignment) @@ -31,8 +67,8 @@ RSpec.describe ApplicationExperiment do end describe "tracking events", :snowplow do - it "doesn't track if excluded" do - subject.exclude { true } + it "doesn't track if we shouldn't track" do + allow(subject).to receive(:should_track?).and_return(false) subject.track(:action) diff --git a/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js b/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js index 26b942c3567..0ca70e0a77e 100644 --- a/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js +++ b/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js @@ -36,6 +36,27 @@ describe('unit_format/formatter_factory', () => { expect(formatNumber(10 ** 7, undefined, 9)).toBe('1.00e+7'); expect(formatNumber(10 ** 7, undefined, 10)).toBe('10,000,000'); }); + + describe('formats with a different locale', () => { + let originalLang; + + beforeAll(() => { + originalLang = document.documentElement.lang; + document.documentElement.lang = 'es'; + }); + + afterAll(() => { + document.documentElement.lang = originalLang; + }); + + it('formats a using the correct thousands separator', () => { + expect(formatNumber(1000000)).toBe('1.000.000'); + }); + + it('formats a using the correct decimal separator', () => { + expect(formatNumber(12.345)).toBe('12,345'); + }); + }); }); describe('suffixFormatter', () => { diff --git a/spec/frontend/projects/pipelines/charts/components/app_spec.js b/spec/frontend/projects/pipelines/charts/components/app_spec.js index 07eb8198edb..6dcfacb46cb 100644 --- a/spec/frontend/projects/pipelines/charts/components/app_spec.js +++ b/spec/frontend/projects/pipelines/charts/components/app_spec.js @@ -1,37 +1,19 @@ import { merge } from 'lodash'; -import { createLocalVue, shallowMount } from '@vue/test-utils'; -import VueApollo from 'vue-apollo'; +import { shallowMount } from '@vue/test-utils'; import { GlTabs, GlTab } from '@gitlab/ui'; -import createMockApollo from 'helpers/mock_apollo_helper'; import setWindowLocation from 'helpers/set_window_location_helper'; import { TEST_HOST } from 'helpers/test_constants'; import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility'; import Component from '~/projects/pipelines/charts/components/app.vue'; import PipelineCharts from '~/projects/pipelines/charts/components/pipeline_charts.vue'; -import getPipelineCountByStatus from '~/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql'; -import getProjectPipelineStatistics from '~/projects/pipelines/charts/graphql/queries/get_project_pipeline_statistics.query.graphql'; -import { mockPipelineCount, mockPipelineStatistics } from '../mock_data'; jest.mock('~/lib/utils/url_utility'); -const projectPath = 'gitlab-org/gitlab'; -const localVue = createLocalVue(); -localVue.use(VueApollo); - const DeploymentFrequencyChartsStub = { name: 'DeploymentFrequencyCharts', render: () => {} }; describe('ProjectsPipelinesChartsApp', () => { let wrapper; - function createMockApolloProvider() { - const requestHandlers = [ - [getPipelineCountByStatus, jest.fn().mockResolvedValue(mockPipelineCount)], - [getProjectPipelineStatistics, jest.fn().mockResolvedValue(mockPipelineStatistics)], - ]; - - return createMockApollo(requestHandlers); - } - function createComponent(mountOptions = {}) { wrapper = shallowMount( Component, @@ -39,11 +21,8 @@ describe('ProjectsPipelinesChartsApp', () => { {}, { provide: { - projectPath, shouldRenderDeploymentFrequencyCharts: false, }, - localVue, - apolloProvider: createMockApolloProvider(), stubs: { DeploymentFrequencyCharts: DeploymentFrequencyChartsStub, }, @@ -62,52 +41,15 @@ describe('ProjectsPipelinesChartsApp', () => { wrapper = null; }); - describe('pipelines charts', () => { - it('displays the pipeline charts', () => { - const chart = wrapper.find(PipelineCharts); - const analytics = mockPipelineStatistics.data.project.pipelineAnalytics; - - const { - totalPipelines: total, - successfulPipelines: success, - failedPipelines: failed, - } = mockPipelineCount.data.project; - - expect(chart.exists()).toBe(true); - expect(chart.props()).toMatchObject({ - counts: { - failed: failed.count, - success: success.count, - total: total.count, - successRatio: (success.count / (success.count + failed.count)) * 100, - }, - lastWeek: { - labels: analytics.weekPipelinesLabels, - totals: analytics.weekPipelinesTotals, - success: analytics.weekPipelinesSuccessful, - }, - lastMonth: { - labels: analytics.monthPipelinesLabels, - totals: analytics.monthPipelinesTotals, - success: analytics.monthPipelinesSuccessful, - }, - lastYear: { - labels: analytics.yearPipelinesLabels, - totals: analytics.yearPipelinesTotals, - success: analytics.yearPipelinesSuccessful, - }, - timesChart: { - labels: analytics.pipelineTimesLabels, - values: analytics.pipelineTimesValues, - }, - }); - }); - }); - - const findDeploymentFrequencyCharts = () => wrapper.find(DeploymentFrequencyChartsStub); const findGlTabs = () => wrapper.find(GlTabs); const findAllGlTab = () => wrapper.findAll(GlTab); const findGlTabAt = (i) => findAllGlTab().at(i); + const findDeploymentFrequencyCharts = () => wrapper.find(DeploymentFrequencyChartsStub); + const findPipelineCharts = () => wrapper.find(PipelineCharts); + + it('renders the pipeline charts', () => { + expect(findPipelineCharts().exists()).toBe(true); + }); describe('when shouldRenderDeploymentFrequencyCharts is true', () => { beforeEach(() => { diff --git a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js index 598055d5828..3e588d46a4f 100644 --- a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js +++ b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js @@ -1,35 +1,37 @@ -import { shallowMount } from '@vue/test-utils'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; import { GlColumnChart } from '@gitlab/ui/dist/charts'; +import createMockApollo from 'helpers/mock_apollo_helper'; import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue'; import CiCdAnalyticsAreaChart from '~/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue'; import PipelineCharts from '~/projects/pipelines/charts/components/pipeline_charts.vue'; -import { - counts, - timesChartData as timesChart, - areaChartData as lastWeek, - areaChartData as lastMonth, - lastYearChartData as lastYear, -} from '../mock_data'; +import getPipelineCountByStatus from '~/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql'; +import getProjectPipelineStatistics from '~/projects/pipelines/charts/graphql/queries/get_project_pipeline_statistics.query.graphql'; +import { mockPipelineCount, mockPipelineStatistics } from '../mock_data'; -describe('ProjectsPipelinesChartsApp', () => { +const projectPath = 'gitlab-org/gitlab'; +const localVue = createLocalVue(); +localVue.use(VueApollo); + +describe('~/projects/pipelines/charts/components/pipeline_charts.vue', () => { let wrapper; + function createMockApolloProvider() { + const requestHandlers = [ + [getPipelineCountByStatus, jest.fn().mockResolvedValue(mockPipelineCount)], + [getProjectPipelineStatistics, jest.fn().mockResolvedValue(mockPipelineStatistics)], + ]; + + return createMockApollo(requestHandlers); + } + beforeEach(() => { wrapper = shallowMount(PipelineCharts, { - propsData: { - counts, - timesChart, - lastWeek, - lastMonth, - lastYear, - }, provide: { - projectPath: 'test/project', - shouldRenderDeploymentFrequencyCharts: true, - }, - stubs: { - DeploymentFrequencyCharts: true, + projectPath, }, + localVue, + apolloProvider: createMockApolloProvider(), }); }); @@ -43,7 +45,12 @@ describe('ProjectsPipelinesChartsApp', () => { const list = wrapper.find(StatisticsList); expect(list.exists()).toBe(true); - expect(list.props('counts')).toBe(counts); + expect(list.props('counts')).toEqual({ + total: 34, + success: 23, + failed: 1, + successRatio: (23 / (23 + 1)) * 100, + }); }); it('displays the commit duration chart', () => { diff --git a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb index b4c230e77e7..edaacf276ed 100644 --- a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb @@ -73,6 +73,22 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl end end + describe '.track_resolve_thread_action' do + subject { described_class.track_resolve_thread_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_RESOLVE_THREAD_ACTION } + end + end + + describe '.track_unresolve_thread_action' do + subject { described_class.track_unresolve_thread_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_UNRESOLVE_THREAD_ACTION } + end + end + describe '.track_create_comment_action' do subject { described_class.track_create_comment_action(note: note) } diff --git a/spec/lib/gitlab/utils/usage_data_spec.rb b/spec/lib/gitlab/utils/usage_data_spec.rb index dfc381d0ef2..12a73f4f4aa 100644 --- a/spec/lib/gitlab/utils/usage_data_spec.rb +++ b/spec/lib/gitlab/utils/usage_data_spec.rb @@ -58,6 +58,16 @@ RSpec.describe Gitlab::Utils::UsageData do expect(described_class.estimate_batch_distinct_count(relation, 'column')).to eq(5) end + it 'yield provided block with PostgresHll::Buckets' do + buckets = Gitlab::Database::PostgresHll::Buckets.new + + allow_next_instance_of(Gitlab::Database::PostgresHll::BatchDistinctCounter) do |instance| + allow(instance).to receive(:execute).and_return(buckets) + end + + expect { |block| described_class.estimate_batch_distinct_count(relation, 'column', &block) }.to yield_with_args(buckets) + end + context 'quasi integration test for different counting parameters' do # HyperLogLog http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf algorithm # used in estimate_batch_distinct_count produce probabilistic @@ -362,4 +372,97 @@ RSpec.describe Gitlab::Utils::UsageData do end end end + + describe '#save_aggregated_metrics', :clean_gitlab_redis_shared_state do + let(:timestamp) { Time.current.to_i } + let(:time_period) { { created_at: 7.days.ago..Date.current } } + let(:metric_name) { 'test_metric' } + let(:method_params) do + { + metric_name: metric_name, + time_period: time_period, + recorded_at_timestamp: timestamp, + data: data + } + end + + context 'with compatible data argument' do + let(:data) { ::Gitlab::Database::PostgresHll::Buckets.new(141 => 1, 56 => 1) } + + it 'persists serialized data in Redis' do + time_period_name = 'weekly' + + expect(described_class).to receive(:time_period_to_human_name).with(time_period).and_return(time_period_name) + Gitlab::Redis::SharedState.with do |redis| + expect(redis).to receive(:set).with("#{metric_name}_#{time_period_name}-#{timestamp}", '{"141":1,"56":1}', ex: 80.hours) + end + + described_class.save_aggregated_metrics(method_params) + end + + context 'error handling' do + before do + allow(Gitlab::Redis::SharedState).to receive(:with).and_raise(::Redis::CommandError) + end + + it 'rescues and reraise ::Redis::CommandError for development and test environments' do + expect { described_class.save_aggregated_metrics(method_params) }.to raise_error ::Redis::CommandError + end + + context 'for environment different than development' do + before do + stub_rails_env('production') + end + + it 'rescues ::Redis::CommandError' do + expect { described_class.save_aggregated_metrics(method_params) }.not_to raise_error + end + end + end + end + + context 'with incompatible data argument' do + let(:data) { 1 } + + context 'for environment different than development' do + before do + stub_rails_env('production') + end + + it 'does not persist data in Redis' do + Gitlab::Redis::SharedState.with do |redis| + expect(redis).not_to receive(:set) + end + + described_class.save_aggregated_metrics(method_params) + end + end + + it 'raises error for development environment' do + expect { described_class.save_aggregated_metrics(method_params) }.to raise_error /Unsupported data type/ + end + end + end + + describe '#time_period_to_human_name' do + it 'translates empty time period as all_time' do + expect(described_class.time_period_to_human_name({})).to eql 'all_time' + end + + it 'translates time period not longer than 7 days as weekly', :aggregate_failures do + days_6_time_period = 6.days.ago..Date.current + days_7_time_period = 7.days.ago..Date.current + + expect(described_class.time_period_to_human_name(column_name: days_6_time_period)).to eql 'weekly' + expect(described_class.time_period_to_human_name(column_name: days_7_time_period)).to eql 'weekly' + end + + it 'translates time period longer than 7 days as monthly', :aggregate_failures do + days_8_time_period = 8.days.ago..Date.current + days_31_time_period = 31.days.ago..Date.current + + expect(described_class.time_period_to_human_name(column_name: days_8_time_period)).to eql 'monthly' + expect(described_class.time_period_to_human_name(column_name: days_31_time_period)).to eql 'monthly' + end + end end diff --git a/spec/services/discussions/resolve_service_spec.rb b/spec/services/discussions/resolve_service_spec.rb index 42c4ef52741..2e30455eb0a 100644 --- a/spec/services/discussions/resolve_service_spec.rb +++ b/spec/services/discussions/resolve_service_spec.rb @@ -24,6 +24,13 @@ RSpec.describe Discussions::ResolveService do expect(discussion).to be_resolved end + it 'tracks thread resolve usage data' do + expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) + .to receive(:track_resolve_thread_action).with(user: user) + + service.execute + end + it 'executes the notification service' do expect_next_instance_of(MergeRequests::ResolvedDiscussionNotificationService) do |instance| expect(instance).to receive(:execute).with(discussion.noteable) @@ -101,6 +108,13 @@ RSpec.describe Discussions::ResolveService do service.execute end + it 'does not track thread resolve usage data' do + expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) + .not_to receive(:track_resolve_thread_action).with(user: user) + + service.execute + end + it 'does not schedule an auto-merge' do expect(AutoMergeProcessWorker).not_to receive(:perform_async) diff --git a/spec/services/discussions/unresolve_service_spec.rb b/spec/services/discussions/unresolve_service_spec.rb new file mode 100644 index 00000000000..6298a00a474 --- /dev/null +++ b/spec/services/discussions/unresolve_service_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Discussions::UnresolveService do + describe "#execute" do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user, developer_projects: [project]) } + let_it_be(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds, source_project: project) } + let(:discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion } + + let(:service) { described_class.new(discussion, user) } + + before do + project.add_developer(user) + discussion.resolve!(user) + end + + it "unresolves the discussion" do + service.execute + + expect(discussion).not_to be_resolved + end + + it "counts the unresolve event" do + expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) + .to receive(:track_unresolve_thread_action).with(user: user) + + service.execute + end + end +end diff --git a/spec/support/gitlab_experiment.rb b/spec/support/gitlab_experiment.rb index 1f283e4f06c..f8375a02b1e 100644 --- a/spec/support/gitlab_experiment.rb +++ b/spec/support/gitlab_experiment.rb @@ -1,4 +1,7 @@ # frozen_string_literal: true +# Require the provided spec helper and matchers. +require 'gitlab/experiment/rspec' + # Disable all caching for experiments in tests. Gitlab::Experiment::Configuration.cache = nil diff --git a/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb b/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb index 460e8d57a2b..b5139bd8c99 100644 --- a/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb @@ -13,6 +13,9 @@ RSpec.shared_examples 'resolvable discussions API' do |parent_type, noteable_typ end it "unresolves discussion if resolved is false" do + expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) + .to receive(:track_unresolve_thread_action).with(user: user) + put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\ "discussions/#{note.discussion_id}", user), params: { resolved: false } diff --git a/yarn.lock b/yarn.lock index 8aba1c81233..fa2a78381d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -840,10 +840,10 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@gitlab/at.js@1.5.5": - version "1.5.5" - resolved "https://registry.yarnpkg.com/@gitlab/at.js/-/at.js-1.5.5.tgz#5f6bfe6baaef360daa9b038fa78798d7a6a916b4" - integrity sha512-282Dn3SPVsUHVDhMsXgfnv+Rzog0uxecjttxGRQvxh25es1+xvkGQFsvJfkSKJ3X1kHVkSjKf+Tt5Rra+Jhp9g== +"@gitlab/at.js@1.5.7": + version "1.5.7" + resolved "https://registry.yarnpkg.com/@gitlab/at.js/-/at.js-1.5.7.tgz#1ee6f838cc4410a1d797770934df91d90df8179e" + integrity sha512-c6ySRK/Ma7lxwpIVbSAF3P+xiTLrNTGTLRx4/pHK111AdFxwgUwrYF6aVZFXvmG65jHOJHoa0eQQ21RW6rm0Rg== "@gitlab/eslint-plugin@6.0.0": version "6.0.0" |