diff options
76 files changed, 1031 insertions, 1161 deletions
diff --git a/.gitlab/ci/static-analysis.gitlab-ci.yml b/.gitlab/ci/static-analysis.gitlab-ci.yml index 85df68e9030..79f578d09dc 100644 --- a/.gitlab/ci/static-analysis.gitlab-ci.yml +++ b/.gitlab/ci/static-analysis.gitlab-ci.yml @@ -24,8 +24,11 @@ static-analysis: extends: - .static-analysis-base - .static-analysis:rules:ee-and-foss + - .use-pg12 stage: test parallel: 4 + variables: + SETUP_DB: "true" script: - run_timed_command "retry yarn install --frozen-lockfile" - scripts/static-analysis @@ -35,17 +38,6 @@ static-analysis: paths: - tmp/feature_flags/ -static-analysis-with-database: - extends: - - .static-analysis-base - - .static-analysis:rules:ee-and-foss - - .use-pg12 - stage: test - script: - - bundle exec rake lint:static_verification_with_database - variables: - SETUP_DB: "true" - static-analysis as-if-foss: extends: - static-analysis @@ -424,7 +424,7 @@ group :test do gem 'webmock', '~> 3.9.1' gem 'rails-controller-testing' gem 'concurrent-ruby', '~> 1.1' - gem 'test-prof', '~> 0.12.0' + gem 'test-prof', '~> 1.0.7' gem 'rspec_junit_formatter' gem 'guard-rspec' diff --git a/Gemfile.lock b/Gemfile.lock index fc13e8d6ecc..dbed17712f7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1251,7 +1251,7 @@ GEM unicode-display_width (~> 1.1, >= 1.1.1) terser (1.0.2) execjs (>= 0.3.0, < 3) - test-prof (0.12.0) + test-prof (1.0.7) test_file_finder (0.1.4) faraday (~> 1.0) text (1.3.1) @@ -1628,7 +1628,7 @@ DEPENDENCIES state_machines-activerecord (~> 0.8.0) sys-filesystem (~> 1.1.6) terser (= 1.0.2) - test-prof (~> 0.12.0) + test-prof (~> 1.0.7) test_file_finder (~> 0.1.3) thin (~> 1.8.0) thrift (>= 0.14.0) diff --git a/app/assets/javascripts/cycle_analytics/constants.js b/app/assets/javascripts/cycle_analytics/constants.js index c1be2ce7096..c205aa1e831 100644 --- a/app/assets/javascripts/cycle_analytics/constants.js +++ b/app/assets/javascripts/cycle_analytics/constants.js @@ -44,7 +44,7 @@ export const METRICS_POPOVER_CONTENT = { }, 'cycle-time': { description: s__( - 'ValueStreamAnalytics|Median time from issue first merge request created to issue closed.', + "ValueStreamAnalytics|Median time from the earliest commit of a linked issue's merge request to when that issue is closed.", ), }, 'new-issue': { description: s__('ValueStreamAnalytics|Number of new issues created.') }, diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue index 45eb2ce51e4..81546151acf 100644 --- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue +++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue @@ -1,5 +1,12 @@ <script> -import { GlFormCheckbox, GlTooltipDirective, GlSprintf, GlIcon } from '@gitlab/ui'; +import { + GlFormCheckbox, + GlTooltipDirective, + GlSprintf, + GlIcon, + GlDropdown, + GlDropdownItem, +} from '@gitlab/ui'; import { formatDate } from '~/lib/utils/datetime_utility'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import { n__ } from '~/locale'; @@ -11,22 +18,22 @@ import { REMOVE_TAG_BUTTON_TITLE, DIGEST_LABEL, CREATED_AT_LABEL, - REMOVE_TAG_BUTTON_DISABLE_TOOLTIP, PUBLISHED_DETAILS_ROW_TEXT, MANIFEST_DETAILS_ROW_TEST, CONFIGURATION_DETAILS_ROW_TEST, MISSING_MANIFEST_WARNING_TOOLTIP, NOT_AVAILABLE_TEXT, NOT_AVAILABLE_SIZE, + MORE_ACTIONS_TEXT, } from '../../constants/index'; -import DeleteButton from '../delete_button.vue'; export default { components: { GlSprintf, GlFormCheckbox, GlIcon, - DeleteButton, + GlDropdown, + GlDropdownItem, ListItem, ClipboardButton, TimeAgoTooltip, @@ -60,11 +67,11 @@ export default { REMOVE_TAG_BUTTON_TITLE, DIGEST_LABEL, CREATED_AT_LABEL, - REMOVE_TAG_BUTTON_DISABLE_TOOLTIP, PUBLISHED_DETAILS_ROW_TEXT, MANIFEST_DETAILS_ROW_TEST, CONFIGURATION_DETAILS_ROW_TEST, MISSING_MANIFEST_WARNING_TOOLTIP, + MORE_ACTIONS_TEXT, }, computed: { formattedSize() { @@ -173,15 +180,26 @@ export default { </span> </template> <template #right-action> - <delete-button - :disabled="isDeleteDisabled" - :title="$options.i18n.REMOVE_TAG_BUTTON_TITLE" - :tooltip-title="$options.i18n.REMOVE_TAG_BUTTON_DISABLE_TOOLTIP" - :tooltip-disabled="tag.canDelete" - data-qa-selector="tag_delete_button" - data-testid="single-delete-button" - @delete="$emit('delete')" - /> + <gl-dropdown + v-if="!isDeleteDisabled" + icon="ellipsis_v" + :text="$options.i18n.MORE_ACTIONS_TEXT" + :text-sr-only="true" + category="tertiary" + no-caret + right + data-testid="additional-actions" + data-qa-selector="more_actions_menu" + > + <gl-dropdown-item + variant="danger" + data-testid="single-delete-button" + data-qa-selector="tag_delete_button" + @click="$emit('delete')" + > + {{ $options.i18n.REMOVE_TAG_BUTTON_TITLE }} + </gl-dropdown-item> + </gl-dropdown> </template> <template v-if="!isInvalidTag" #details-published> diff --git a/app/assets/javascripts/registry/explorer/constants/common.js b/app/assets/javascripts/registry/explorer/constants/common.js index dc71ef8450b..f7beec2c935 100644 --- a/app/assets/javascripts/registry/explorer/constants/common.js +++ b/app/assets/javascripts/registry/explorer/constants/common.js @@ -1,3 +1,4 @@ -import { s__ } from '~/locale'; +import { s__, __ } from '~/locale'; export const ROOT_IMAGE_TEXT = s__('ContainerRegistry|Root image'); +export const MORE_ACTIONS_TEXT = __('More actions'); diff --git a/app/assets/javascripts/registry/explorer/constants/details.js b/app/assets/javascripts/registry/explorer/constants/details.js index 0836260b71e..19e1a75fb2f 100644 --- a/app/assets/javascripts/registry/explorer/constants/details.js +++ b/app/assets/javascripts/registry/explorer/constants/details.js @@ -30,7 +30,7 @@ export const CONFIGURATION_DETAILS_ROW_TEST = s__( 'ContainerRegistry|Configuration digest: %{digest}', ); -export const REMOVE_TAG_BUTTON_TITLE = s__('ContainerRegistry|Remove tag'); +export const REMOVE_TAG_BUTTON_TITLE = s__('ContainerRegistry|Delete tag'); export const REMOVE_TAGS_BUTTON_TITLE = s__('ContainerRegistry|Delete selected tags'); export const REMOVE_TAG_CONFIRMATION_TEXT = s__( @@ -61,10 +61,6 @@ export const ADMIN_GARBAGE_COLLECTION_TIP = s__( 'ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage.', ); -export const REMOVE_TAG_BUTTON_DISABLE_TOOLTIP = s__( - 'ContainerRegistry|Deletion disabled due to missing or insufficient permissions.', -); - export const MISSING_MANIFEST_WARNING_TOOLTIP = s__( 'ContainerRegistry|Invalid tag: missing manifest digest', ); diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 23254fcc2eb..381421cdc23 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -111,7 +111,7 @@ Sidebar.prototype.toggleTodo = function (e) { }; Sidebar.prototype.sidebarCollapseClicked = function (e) { - if ($(e.currentTarget).hasClass('dont-change-state')) { + if ($(e.currentTarget).hasClass('js-dont-change-state')) { return; } const sidebar = e.data; diff --git a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue index 35476cc411e..c8513a0b803 100644 --- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue +++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue @@ -9,7 +9,6 @@ import RunnerList from '../components/runner_list.vue'; import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue'; import RunnerName from '../components/runner_name.vue'; import RunnerPagination from '../components/runner_pagination.vue'; -import RunnerTypeHelp from '../components/runner_type_help.vue'; import { statusTokenConfig } from '../components/search_tokens/status_token_config'; import { tagTokenConfig } from '../components/search_tokens/tag_token_config'; import { typeTokenConfig } from '../components/search_tokens/type_token_config'; @@ -29,7 +28,6 @@ export default { RunnerFilteredSearchBar, RunnerList, RunnerManualSetupHelp, - RunnerTypeHelp, RunnerName, RunnerPagination, }, @@ -128,17 +126,10 @@ export default { </script> <template> <div> - <div class="row"> - <div class="col-sm-6"> - <runner-type-help /> - </div> - <div class="col-sm-6"> - <runner-manual-setup-help - :registration-token="registrationToken" - :type="$options.INSTANCE_TYPE" - /> - </div> - </div> + <runner-manual-setup-help + :registration-token="registrationToken" + :type="$options.INSTANCE_TYPE" + /> <runner-filtered-search-bar v-model="search" diff --git a/app/assets/javascripts/runner/components/runner_type_help.vue b/app/assets/javascripts/runner/components/runner_type_help.vue deleted file mode 100644 index 2326f66ebb8..00000000000 --- a/app/assets/javascripts/runner/components/runner_type_help.vue +++ /dev/null @@ -1,74 +0,0 @@ -<script> -import { - INSTANCE_TYPE, - GROUP_TYPE, - PROJECT_TYPE, - I18N_INSTANCE_RUNNER_DESCRIPTION, - I18N_GROUP_RUNNER_DESCRIPTION, - I18N_PROJECT_RUNNER_DESCRIPTION, - I18N_LOCKED_RUNNER_DESCRIPTION, - I18N_PAUSED_RUNNER_DESCRIPTION, -} from '../constants'; -import RunnerTypeBadge from './runner_type_badge.vue'; -import RunnerStateLockedBadge from './runner_state_locked_badge.vue'; -import RunnerStatePausedBadge from './runner_state_paused_badge.vue'; - -export default { - components: { - RunnerTypeBadge, - RunnerStateLockedBadge, - RunnerStatePausedBadge, - }, - runnerTypes: { - INSTANCE_TYPE, - GROUP_TYPE, - PROJECT_TYPE, - }, - i18n: { - I18N_INSTANCE_RUNNER_DESCRIPTION, - I18N_GROUP_RUNNER_DESCRIPTION, - I18N_PROJECT_RUNNER_DESCRIPTION, - I18N_LOCKED_RUNNER_DESCRIPTION, - I18N_PAUSED_RUNNER_DESCRIPTION, - }, -}; -</script> - -<template> - <div class="bs-callout"> - <p>{{ __('Runners are processes that pick up and execute CI/CD jobs for GitLab.') }}</p> - <p> - {{ - __( - 'You can register runners as separate users, on separate servers, and on your local machine. Register as many runners as you want.', - ) - }} - </p> - - <div> - <span> {{ __('Runners can be:') }}</span> - <ul> - <li> - <runner-type-badge :type="$options.runnerTypes.INSTANCE_TYPE" size="sm" /> - - {{ $options.i18n.I18N_INSTANCE_RUNNER_DESCRIPTION }} - </li> - <li> - <runner-type-badge :type="$options.runnerTypes.GROUP_TYPE" size="sm" /> - - {{ $options.i18n.I18N_GROUP_RUNNER_DESCRIPTION }} - </li> - <li> - <runner-type-badge :type="$options.runnerTypes.PROJECT_TYPE" size="sm" /> - - {{ $options.i18n.I18N_PROJECT_RUNNER_DESCRIPTION }} - </li> - <li> - <runner-state-locked-badge size="sm" /> - - {{ $options.i18n.I18N_LOCKED_RUNNER_DESCRIPTION }} - </li> - <li> - <runner-state-paused-badge size="sm" /> - - {{ $options.i18n.I18N_PAUSED_RUNNER_DESCRIPTION }} - </li> - </ul> - </div> - </div> -</template> diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue index 083b2666b7b..4bb28796dfa 100644 --- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue +++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue @@ -10,7 +10,6 @@ import RunnerList from '../components/runner_list.vue'; import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue'; import RunnerName from '../components/runner_name.vue'; import RunnerPagination from '../components/runner_pagination.vue'; -import RunnerTypeHelp from '../components/runner_type_help.vue'; import { statusTokenConfig } from '../components/search_tokens/status_token_config'; import { typeTokenConfig } from '../components/search_tokens/type_token_config'; @@ -36,7 +35,6 @@ export default { RunnerList, RunnerManualSetupHelp, RunnerName, - RunnerTypeHelp, RunnerPagination, }, props: { @@ -146,17 +144,7 @@ export default { <template> <div> - <div class="row"> - <div class="col-sm-6"> - <runner-type-help /> - </div> - <div class="col-sm-6"> - <runner-manual-setup-help - :registration-token="registrationToken" - :type="$options.GROUP_TYPE" - /> - </div> - </div> + <runner-manual-setup-help :registration-token="registrationToken" :type="$options.GROUP_TYPE" /> <runner-filtered-search-bar v-model="search" diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue index f7e76cc2b7f..d5782e4b371 100644 --- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue +++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue @@ -41,7 +41,7 @@ export default { computed: { buttonClasses() { return this.collapsed - ? 'btn-blank btn-todo sidebar-collapsed-icon dont-change-state' + ? 'btn-blank btn-todo sidebar-collapsed-icon js-dont-change-state' : 'gl-button btn btn-default btn-todo issuable-header-btn float-right'; }, buttonLabel() { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue index b2c9d28a88b..c4e5fa06cd6 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue @@ -60,7 +60,7 @@ export default { this.isCollapsed ? s__('mrWidget|Show %{widget} details') : s__('mrWidget|Hide %{widget} details'), - { widget: this.$options.name }, + { widget: this.$options.label || this.$options.name }, ); }, statusIconName() { @@ -120,7 +120,7 @@ export default { <section class="media-section" data-testid="widget-extension"> <div class="media gl-p-5"> <status-icon - :name="$options.name" + :name="$options.label || $options.name" :is-loading="isLoadingSummary" :icon-name="statusIconName" /> @@ -133,7 +133,10 @@ export default { </template> <div v-else v-safe-html="summary(collapsedData)"></div> </div> - <actions :widget="$options.name" :tertiary-buttons="tertiaryActionsButtons" /> + <actions + :widget="$options.label || $options.name" + :tertiary-buttons="tertiaryActionsButtons" + /> <div class="gl-float-right gl-align-self-center gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3" > diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js index 0b96b2844c2..21e0b95431b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js @@ -7,6 +7,7 @@ export default { // Give the extension a name // Make it easier to track in Vue dev tools name: 'Issues', + label: 'Issues', // Add an array of props // These then get mapped to values stored in the MR Widget store props: ['targetProjectFullPath', 'conflictsDocsPath'], diff --git a/app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue b/app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue index 5c3a6852219..6538de085b0 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue @@ -62,7 +62,7 @@ export default { <div> <clipboard-button v-if="!isLoading" - css-class="sidebar-collapsed-icon dont-change-state gl-rounded-0! gl-hover-bg-transparent" + css-class="sidebar-collapsed-icon js-dont-change-state gl-rounded-0! gl-hover-bg-transparent" v-bind="clipboardProps" /> diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index d1c9e772f50..279b2718d0e 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -862,11 +862,6 @@ module Ci self.duration = Gitlab::Ci::Pipeline::Duration.from_pipeline(self) end - def execute_hooks - project.execute_hooks(pipeline_data, :pipeline_hooks) if project.has_active_hooks?(:pipeline_hooks) - project.execute_integrations(pipeline_data, :pipeline_hooks) if project.has_active_integrations?(:pipeline_hooks) - end - # All the merge requests for which the current pipeline runs/ran against def all_merge_requests @all_merge_requests ||= @@ -1252,12 +1247,6 @@ module Ci messages.build(severity: severity, content: content) end - def pipeline_data - strong_memoize(:pipeline_data) do - Gitlab::DataBuilder::Pipeline.build(self) - end - end - def merge_request_diff_sha return unless merge_request? diff --git a/app/services/ci/pipelines/hook_service.rb b/app/services/ci/pipelines/hook_service.rb new file mode 100644 index 00000000000..629ed7e1ebd --- /dev/null +++ b/app/services/ci/pipelines/hook_service.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Ci + module Pipelines + class HookService + include Gitlab::Utils::StrongMemoize + + HOOK_NAME = :pipeline_hooks + + def initialize(pipeline) + @pipeline = pipeline + end + + def execute + project.execute_hooks(hook_data, HOOK_NAME) if project.has_active_hooks?(HOOK_NAME) + project.execute_integrations(hook_data, HOOK_NAME) if project.has_active_integrations?(HOOK_NAME) + end + + private + + attr_reader :pipeline + + def project + @project ||= pipeline.project + end + + def hook_data + strong_memoize(:hook_data) do + Gitlab::DataBuilder::Pipeline.build(pipeline) + end + end + end + end +end diff --git a/app/services/ci/stuck_builds/drop_service.rb b/app/services/ci/stuck_builds/drop_pending_service.rb index 0f8375e0cf1..4653e701973 100644 --- a/app/services/ci/stuck_builds/drop_service.rb +++ b/app/services/ci/stuck_builds/drop_pending_service.rb @@ -2,7 +2,7 @@ module Ci module StuckBuilds - class DropService + class DropPendingService include DropHelpers BUILD_PENDING_OUTDATED_TIMEOUT = 1.day @@ -10,7 +10,7 @@ module Ci BUILD_LOOKBACK = 5.days def execute - Gitlab::AppLogger.info "#{self.class}: Cleaning stuck builds" + Gitlab::AppLogger.info "#{self.class}: Cleaning pending timed-out builds" drop( pending_builds(BUILD_PENDING_OUTDATED_TIMEOUT.ago), diff --git a/app/services/terraform/remote_state_handler.rb b/app/services/terraform/remote_state_handler.rb index e9a13cee764..f13477b8b34 100644 --- a/app/services/terraform/remote_state_handler.rb +++ b/app/services/terraform/remote_state_handler.rb @@ -2,8 +2,6 @@ module Terraform class RemoteStateHandler < BaseService - include Gitlab::OptimisticLocking - StateLockedError = Class.new(StandardError) UnauthorizedError = Class.new(StandardError) @@ -60,7 +58,7 @@ module Terraform private def retrieve_with_lock(find_only: false) - create_or_find!(find_only: find_only).tap { |state| retry_optimistic_lock(state, name: 'terraform_remote_state_handler_retrieve') { |state| yield state } } + create_or_find!(find_only: find_only).tap { |state| state.with_lock { yield state } } end def create_or_find!(find_only:) diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index f5f5674190c..62539bfeffd 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -75,7 +75,7 @@ #js-reference-entry-point - if issuable_type == 'merge_request' .sub-block.js-sidebar-source-branch - .sidebar-collapsed-icon.dont-change-state + .sidebar-collapsed-icon.js-dont-change-state = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport') .gl-display-flex.gl-align-items-center.gl-justify-content-space-between.gl-mb-2.hide-collapsed %span.gl-overflow-hidden.gl-text-overflow-ellipsis.gl-white-space-nowrap diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml index 8ed40f74c88..c66ba5ba2e1 100644 --- a/app/views/shared/milestones/_sidebar.html.haml +++ b/app/views/shared/milestones/_sidebar.html.haml @@ -161,7 +161,7 @@ - milestone_ref = milestone.try(:to_reference, full: true) - if milestone_ref.present? .block.reference - .sidebar-collapsed-icon.dont-change-state + .sidebar-collapsed-icon.js-dont-change-state = clipboard_button(text: milestone_ref, title: s_('MilestoneSidebar|Copy reference'), placement: "left", boundary: 'viewport') .cross-project-reference.hide-collapsed %span.gl-display-inline-block.gl-text-truncate diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb index 322f92d376b..c67f3860a50 100644 --- a/app/workers/pipeline_hooks_worker.rb +++ b/app/workers/pipeline_hooks_worker.rb @@ -12,9 +12,10 @@ class PipelineHooksWorker # rubocop:disable Scalability/IdempotentWorker # rubocop: disable CodeReuse/ActiveRecord def perform(pipeline_id) - Ci::Pipeline - .find_by(id: pipeline_id) - .try(:execute_hooks) + pipeline = Ci::Pipeline.find_by(id: pipeline_id) + return unless pipeline + + Ci::Pipelines::HookService.new(pipeline).execute end # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb index 72fcf06dce1..f8f1d8c60b3 100644 --- a/app/workers/stuck_ci_jobs_worker.rb +++ b/app/workers/stuck_ci_jobs_worker.rb @@ -20,7 +20,7 @@ class StuckCiJobsWorker # rubocop:disable Scalability/IdempotentWorker Ci::StuckBuilds::DropScheduledWorker.perform_in(40.minutes) try_obtain_lease do - Ci::StuckBuilds::DropService.new.execute + Ci::StuckBuilds::DropPendingService.new.execute end end diff --git a/bin/rails b/bin/rails index 07396602377..5badb2fde0c 100755 --- a/bin/rails +++ b/bin/rails @@ -1,4 +1,9 @@ #!/usr/bin/env ruby +begin + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') +end APP_PATH = File.expand_path('../config/application', __dir__) require_relative '../config/boot' require 'rails/commands' @@ -1,4 +1,9 @@ #!/usr/bin/env ruby +begin + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') +end require_relative '../config/boot' require 'rake' Rake.application.run diff --git a/config/feature_flags/development/disable_joins_upstream_downstream_projects.yml b/config/feature_flags/development/disable_joins_upstream_downstream_projects.yml deleted file mode 100644 index 05023dc1c79..00000000000 --- a/config/feature_flags/development/disable_joins_upstream_downstream_projects.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: disable_joins_upstream_downstream_projects -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71247 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341791 -milestone: '14.4' -type: development -group: group::sharding -default_enabled: false diff --git a/config/metrics/counts_all/20210216182112_sast_jobs.yml b/config/metrics/counts_all/20210216182112_sast_jobs.yml deleted file mode 100644 index e14d89c3fdb..00000000000 --- a/config/metrics/counts_all/20210216182112_sast_jobs.yml +++ /dev/null @@ -1,22 +0,0 @@ ---- -data_category: operational -key_path: counts.sast_jobs -description: Count of SAST CI jobs for the month. Job names ending in '-sast' -product_section: sec -product_stage: secure -product_group: group::static analysis -product_category: static_application_security_testing -value_type: number -status: broken -repair_issue_url: tbd -time_frame: all -data_source: database -distribution: -- ce -- ee -tier: -- free -- premium -- ultimate -performance_indicator_type: [] -milestone: "<13.9" diff --git a/config/metrics/counts_all/20210216182114_secret_detection_jobs.yml b/config/metrics/counts_all/20210216182114_secret_detection_jobs.yml deleted file mode 100644 index 2278fea0b6a..00000000000 --- a/config/metrics/counts_all/20210216182114_secret_detection_jobs.yml +++ /dev/null @@ -1,22 +0,0 @@ ---- -data_category: operational -key_path: counts.secret_detection_jobs -description: Count of all 'secret-detection' CI jobs. -product_section: sec -product_stage: secure -product_group: group::static analysis -product_category: secret_detection -value_type: number -status: broken -repair_issue_url: tbd -time_frame: all -data_source: database -distribution: -- ce -- ee -tier: -- free -- premium -- ultimate -performance_indicator_type: [] -milestone: "<13.9" diff --git a/doc/development/contributing/design.md b/doc/development/contributing/design.md index 6831fb186d4..0f4a3ba9597 100644 --- a/doc/development/contributing/design.md +++ b/doc/development/contributing/design.md @@ -46,17 +46,11 @@ Check these aspects both when _designing_ and _reviewing_ UI changes. - Use appropriate [components](https://design.gitlab.com/components/overview) and [data visualizations](https://design.gitlab.com/data-visualization/overview). -### States - -- Account for all applicable states ([error](https://design.gitlab.com/content/error-messages), - rest, loading, focus, hover, selected, disabled). -- Account for states dependent on data size ([empty](https://design.gitlab.com/regions/empty-states), - some data, and lots of data). -- Account for states dependent on user role, user preferences, and subscription. -- Consider animations and transitions, and follow their [guidelines](https://design.gitlab.com/product-foundations/motion). - ### Visual design +Check visual design properties using your browser's _elements inspector_ ([Chrome](https://developer.chrome.com/docs/devtools/css/), +[Firefox](https://developer.mozilla.org/en-US/docs/Tools/Page_Inspector/How_to/Open_the_Inspector)). + - Use recommended [colors](https://design.gitlab.com/product-foundations/colors) and [typography](https://design.gitlab.com/product-foundations/type-fundamentals). - Follow [layout guidelines](https://design.gitlab.com/layout/grid). @@ -68,14 +62,33 @@ Check these aspects both when _designing_ and _reviewing_ UI changes. [^1]: You're not required to design for [dark mode](../../user/profile/preferences.md#dark-mode) while the feature is in [alpha](https://about.gitlab.com/handbook/product/gitlab-the-product/#alpha). The [UX Foundations team](https://about.gitlab.com/direction/ecosystem/foundations/) plans to improve the dark mode in the future. Until we integrate [Pajamas](https://design.gitlab.com/) components into the product and the underlying design strategy is in place to support dark mode, we cannot guarantee that we won't introduce bugs and debt to this mode. At your discretion, evaluate the need to create dark mode patches. +### States + +Check states using your browser's _styles inspector_ to toggle CSS pseudo-classes +like `:hover` and others ([Chrome](https://developer.chrome.com/docs/devtools/css/reference/#pseudo-class), +[Firefox](https://developer.mozilla.org/en-US/docs/Tools/Page_Inspector/How_to/Examine_and_edit_CSS#viewing_common_pseudo-classes)). + +- Account for all applicable states ([error](https://design.gitlab.com/content/error-messages), + rest, loading, focus, hover, selected, disabled). +- Account for states dependent on data size ([empty](https://design.gitlab.com/regions/empty-states), + some data, and lots of data). +- Account for states dependent on user role, user preferences, and subscription. +- Consider animations and transitions, and follow their [guidelines](https://design.gitlab.com/product-foundations/motion). + ### Responsive +Check responsive behavior using your browser's _responsive mode_ ([Chrome](https://developer.chrome.com/docs/devtools/device-mode/#viewport), +[Firefox](https://developer.mozilla.org/en-US/docs/Tools/Responsive_Design_Mode)). + - Account for resizing, collapsing, moving, or wrapping of elements across all breakpoints (even if larger viewports are prioritized). - Provide the same information and actions in all breakpoints. ### Accessibility +Check accessibility using your browser's _accessibility inspector_ ([Chrome](https://developer.chrome.com/docs/devtools/accessibility/reference/), +[Firefox](https://developer.mozilla.org/en-US/docs/Tools/Accessibility_inspector#accessing_the_accessibility_inspector)). + - Conform to level AA of the World Wide Web Consortium (W3C) [Web Content Accessibility Guidelines 2.1](https://www.w3.org/TR/WCAG21/), according to our [statement of compliance](https://design.gitlab.com/accessibility/a11y). - Follow accessibility [best practices](https://design.gitlab.com/accessibility/best-practices) @@ -83,6 +96,8 @@ Check these aspects both when _designing_ and _reviewing_ UI changes. ### Handoff +When the design is ready, _before_ starting its implementation: + - Share design specifications in the related issue, preferably through a [Figma link](https://help.figma.com/hc/en-us/articles/360040531773-Share-Files-with-anyone-using-Link-Sharing#Copy_links) link or [GitLab Designs feature](../../user/project/issues/design_management.md#the-design-management-section). See [when you should use each tool](https://about.gitlab.com/handbook/engineering/ux/product-designer/#deliver). @@ -96,6 +111,8 @@ Check these aspects both when _designing_ and _reviewing_ UI changes. ### Follow-ups +At any moment, but usually _during_ or _after_ the design's implementation: + - Contribute [issues to Pajamas](https://design.gitlab.com/get-started/contribute#contribute-an-issue) for additions or enhancements to the design system. - Create issues with the [`~UX debt`](issue_workflow.md#technical-and-ux-debt) diff --git a/doc/development/snowplow/implementation.md b/doc/development/snowplow/implementation.md new file mode 100644 index 00000000000..6cc4de5bf21 --- /dev/null +++ b/doc/development/snowplow/implementation.md @@ -0,0 +1,519 @@ +--- +stage: Growth +group: Product Intelligence +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 +--- + +# Implement Snowplow tracking + +This guide describes how to implement and test Snowplow tracking using JavaScript and Ruby trackers. + +## Snowplow JavaScript frontend tracking + +GitLab provides a `Tracking` interface that wraps the [Snowplow JavaScript tracker](https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/javascript-trackers/) to track custom events. For the recommended implementation type, see [Usage recommendations](#usage-recommendations). + +Tracking implementations must have an `action` and a `category`. You can provide additional [structured event taxonomy](index.md#structured-event-taxonomy) categories with an `extra` object that accepts key-value pairs. + +| Field | Type | Default value | Description | +|:-----------|:-------|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `category` | string | `document.body.dataset.page` | Page or subsection of a page in which events are captured. | +| `action` | string | generic | Action the user is taking. Clicks must be `click` and activations must be `activate`. For example, focusing a form field is `activate_form_input`, and clicking a button is `click_button`. | +| `data` | object | `{}` | Additional data such as `label`, `property`, `value`, `context` as described in [Structured event taxonomy](index.md#structured-event-taxonomy), and `extra` (key-value pairs object). | + +### Usage recommendations + +- Use [data attributes](#implement-data-attribute-tracking) on HTML elements that emit `click`, `show.bs.dropdown`, or `hide.bs.dropdown` events. +- Use the [Vue mixin](#implement-vue-component-tracking) for tracking custom events, or if the supported events for data attributes are not propagating. +- Use the [tracking class](#implement-raw-javascript-tracking) when tracking raw JavaScript files. + +### Implement data attribute tracking + +To implement tracking for HAML or Vue templates, add a [`data-track` attribute](#data-track-attributes) to the element. + +The following example shows `data-track-*` attributes assigned to a button: + +```haml +%button.btn{ data: { track: { action: "click_button", label: "template_preview", property: "my-template" } } } +``` + +```html +<button class="btn" + data-track-action="click_button" + data-track-label="template_preview" + data-track-property="my-template" + data-track-extra='{ "template_variant": "primary" }' +/> +``` + +#### `data-track` attributes + +| Attribute | Required | Description | +|:----------------------|:---------|:------------| +| `data-track-action` | true | Action the user is taking. Clicks must be prepended with `click` and activations must be prepended with `activate`. For example, focusing a form field is `activate_form_input` and clicking a button is `click_button`. Replaces `data-track-event`, which was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/290962) in GitLab 13.11. | +| `data-track-label` | false | The `label` as described in [structured event taxonomy](index.md#structured-event-taxonomy). | +| `data-track-property` | false | The `property` as described in [structured event taxonomy](index.md#structured-event-taxonomy). | +| `data-track-value` | false | The `value` as described in [structured event taxonomy](index.md#structured-event-taxonomy). If omitted, this is the element's `value` property or `undefined`. For checkboxes, the default value is the element's checked attribute or `0` when unchecked. | +| `data-track-extra` | false | A key-value pairs object passed as a valid JSON string. This is added to the `extra` property in our [`gitlab_standard`](schemas.md#gitlab_standard) schema. | +| `data-track-context` | false | The `context` as described in our [Structured event taxonomy](index.md#structured-event-taxonomy). | + +#### Event listeners + +Event listeners are bound at the document level to handle click events in elements with data attributes. This allows them to be handled on re-rendering and changes to the DOM. Because of the way these events are bound, click events should not stop from propagating up the DOM tree. If click events are stopped from propagating, you must implement listeners and follow the instructions in [Implement Vue component tracking](#implement-vue-component-tracking) or [Implement raw JavaScript tracking](#implement-raw-javascript-tracking). + +#### Available helpers + +```ruby +tracking_attrs(label, action, property) # { data: { track_label... } } + +%button{ **tracking_attrs('main_navigation', 'click_button', 'navigation') } +``` + +#### Caveats + +When using the GitLab helper method [`nav_link`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/helpers/tab_helper.rb#L76) be sure to wrap `html_options` under the `html_options` keyword argument. +Be careful, as this behavior can be confused with the `ActionView` helper method [`link_to`](https://api.rubyonrails.org/v5.2.3/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to) that does not require additional wrapping of `html_options` + +```ruby +# Bad += nav_link(controller: ['dashboard/groups', 'explore/groups'], data: { track_label: "explore_groups", track_action: "click_button" }) + +# Good += nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { data: { track_label: "explore_groups", track_action: "click_button" } }) + +# Good (other helpers) += link_to explore_groups_path, title: _("Explore"), data: { track_label: "explore_groups", track_action: "click_button" } +``` + +### Implement Vue component tracking + +For custom event tracking, use a Vue `mixin` in components. Vue `mixin` exposes the `Tracking.event` static method and the `track` method called from components or templates. You can specify tracking options in `data` or `computed`. These options override any defaults and allow the values to be dynamic from props or based on state. + +Default options are passed when an event is tracked from the component. If you don't specify an option, the default `document.body.dataset.page` is used. The default options are: + +- `category` +- `label` +- `property` +- `value` + +To implement Vue component tracking: + +1. Import the `Tracking` library and request a `mixin`: + + ```javascript + import Tracking from '~/tracking'; + const trackingMixin = Tracking.mixin; + ``` + +1. Provide categories to track the event from the component. For example, to track all events in a component with a label, use the `label` category: + + ```javascript + import Tracking from '~/tracking'; + const trackingMixin = Tracking.mixin({ label: 'right_sidebar' }); + ``` + +1. In the component, declare the Vue `mixin`. + + ```javascript + export default { + mixins: [trackingMixin], + // ...[component implementation]... + data() { + return { + expanded: false, + tracking: { + label: 'left_sidebar', + }, + }; + }, + }; + ``` + +1. To receive event data as a tracking object or computed property: + - Declare it in the `data` function. Use a `tracking` object when default event properties are dynamic or provided at runtime: + + ```javascript + export default { + name: 'RightSidebar', + mixins: [Tracking.mixin()], + data() { + return { + tracking: { + label: 'right_sidebar', + // category: '', + // property: '', + // value: '', + // experiment: '', + // extra: {}, + }, + }; + }, + }; + ``` + + - Declare it in the event data in the `track` function. This object merges with any previously provided options: + + ```javascript + this.track('click_button', { + label: 'right_sidebar', + }); + ``` + +1. Optional. Use the `track` method in a template: + + ```html + <template> + <div> + <button data-testid="toggle" @click="toggle">Toggle</button> + + <div v-if="expanded"> + <p>Hello world!</p> + <button @click="track('click_action')">Track another event</button> + </div> + </div> + </template> + ``` + +#### Implementation example + +```javascript +export default { + name: 'RightSidebar', + mixins: [Tracking.mixin({ label: 'right_sidebar' })], + data() { + return { + expanded: false, + }; + }, + methods: { + toggle() { + this.expanded = !this.expanded; + // Additional data will be merged, like `value` below + this.track('click_toggle', { value: Number(this.expanded) }); + } + } +}; +``` + +#### Testing example + +```javascript +import { mockTracking } from 'helpers/tracking_helper'; +// mockTracking(category, documentOverride, spyMethod) + +describe('RightSidebar.vue', () => { + let trackingSpy; + let wrapper; + + beforeEach(() => { + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + }); + + const findToggle = () => wrapper.find('[data-testid="toggle"]'); + + it('tracks turning off toggle', () => { + findToggle().trigger('click'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_toggle', { + label: 'right_sidebar', + value: 0, + }); + }); +}); +``` + +### Implement raw JavaScript tracking + +To call custom event tracking and instrumentation directly from the JavaScript file, call the `Tracking.event` static function. + +The following example demonstrates tracking a click on a button by manually calling `Tracking.event`. + +```javascript +import Tracking from '~/tracking'; + +const button = document.getElementById('create_from_template_button'); + +button.addEventListener('click', () => { + Tracking.event('dashboard:projects:index', 'click_button', { + label: 'create_from_template', + property: 'template_preview', + extra: { + templateVariant: 'primary', + valid: 1, + }, + }); +}); +``` + +#### Testing example + +```javascript +import Tracking from '~/tracking'; + +describe('MyTracking', () => { + let wrapper; + + beforeEach(() => { + jest.spyOn(Tracking, 'event'); + }); + + const findButton = () => wrapper.find('[data-testid="create_from_template"]'); + + it('tracks event', () => { + findButton().trigger('click'); + + expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', { + label: 'create_from_template', + property: 'template_preview', + extra: { + templateVariant: 'primary', + valid: true, + }, + }); + }); +}); +``` + +### Form tracking + +Enable Snowplow automatic [form tracking](https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/javascript-trackers/javascript-tracker/javascript-tracker-v2/tracking-specific-events/#form-tracking) by calling `Tracking.enableFormTracking` (after the DOM is ready) and providing a `config` object that includes at least one of the following elements: + +- `forms`: determines which forms are tracked, and are identified by the CSS class name. +- `fields`: determines which fields inside the tracked forms are tracked, and are identified by the field `name`. + +An optional list of contexts can be provided as the second argument. +Note that our [`gitlab_standard`](schemas.md#gitlab_standard) schema is excluded from these events. + +```javascript +Tracking.enableFormTracking({ + forms: { allow: ['sign-in-form', 'password-recovery-form'] }, + fields: { allow: ['terms_and_conditions', 'newsletter_agreement'] }, +}); +``` + +#### Testing example + +```javascript +import Tracking from '~/tracking'; + +describe('MyFormTracking', () => { + let formTrackingSpy; + + beforeEach(() => { + formTrackingSpy = jest + .spyOn(Tracking, 'enableFormTracking') + .mockImplementation(() => null); + }); + + it('initialized with the correct configuration', () => { + expect(formTrackingSpy).toHaveBeenCalledWith({ + forms: { allow: ['sign-in-form', 'password-recovery-form'] }, + fields: { allow: ['terms_and_conditions', 'newsletter_agreement'] }, + }); + }); +}); +``` + +## Implement Snowplow Ruby (Backend) tracking + +GitLab provides `Gitlab::Tracking`, an interface that wraps the [Snowplow Ruby Tracker](https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/ruby-tracker/) for tracking custom events. + +Custom event tracking and instrumentation can be added by directly calling the `GitLab::Tracking.event` class method, which accepts the following arguments: + +| argument | type | default value | description | +|------------|---------------------------|---------------|-----------------------------------------------------------------------------------------------------------------------------------| +| `category` | String | | Area or aspect of the application. This could be `HealthCheckController` or `Lfs::FileTransformer` for instance. | +| `action` | String | | The action being taken, which can be anything from a controller action like `create` to something like an Active Record callback. | +| `label` | String | nil | As described in [Structured event taxonomy](index.md#structured-event-taxonomy). | +| `property` | String | nil | As described in [Structured event taxonomy](index.md#structured-event-taxonomy). | +| `value` | Numeric | nil | As described in [Structured event taxonomy](index.md#structured-event-taxonomy). | +| `context` | Array\[SelfDescribingJSON\] | nil | An array of custom contexts to send with this event. Most events should not have any custom contexts. | +| `project` | Project | nil | The project associated with the event. | +| `user` | User | nil | The user associated with the event. | +| `namespace` | Namespace | nil | The namespace associated with the event. | +| `extra` | Hash | `{}` | Additional keyword arguments are collected into a hash and sent with the event. | + +Tracking can be viewed as either tracking user behavior, or can be used for instrumentation to monitor and visualize performance over time in an area or aspect of code. + +For example: + +```ruby +class Projects::CreateService < BaseService + def execute + project = Project.create(params) + + Gitlab::Tracking.event('Projects::CreateService', 'create_project', label: project.errors.full_messages.to_sentence, + property: project.valid?.to_s, project: project, user: current_user, namespace: namespace) + end +end +``` + +### Unit testing + +Use the `expect_snowplow_event` helper when testing backend Snowplow events. See [testing best practices]( +https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#test-snowplow-events) for details. + +### Performance + +We use the [AsyncEmitter](https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/ruby-tracker/emitters/#the-asyncemitter-class) when tracking events, which allows for instrumentation calls to be run in a background thread. This is still an active area of development. + +## Develop and test Snowplow + +There are several tools for developing and testing a Snowplow event. + +| Testing Tool | Frontend Tracking | Backend Tracking | Local Development Environment | Production Environment | Production Environment | +|----------------------------------------------|--------------------|---------------------|-------------------------------|------------------------|------------------------| +| Snowplow Analytics Debugger Chrome Extension | **{check-circle}** | **{dotted-circle}** | **{check-circle}** | **{check-circle}** | **{check-circle}** | +| Snowplow Inspector Chrome Extension | **{check-circle}** | **{dotted-circle}** | **{check-circle}** | **{check-circle}** | **{check-circle}** | +| Snowplow Micro | **{check-circle}** | **{check-circle}** | **{check-circle}** | **{dotted-circle}** | **{dotted-circle}** | +| Snowplow Mini | **{check-circle}** | **{check-circle}** | **{dotted-circle}** | **{status_preparing}** | **{status_preparing}** | + +**Legend** + +**{check-circle}** Available, **{status_preparing}** In progress, **{dotted-circle}** Not Planned + +### Test frontend events + +To test frontend events in development: + +- [Enable Snowplow tracking in the Admin Area](index.md#enable-snowplow-tracking). +- Turn off any ad blockers that would prevent Snowplow JS from loading in your environment. +- Turn off "Do Not Track" (DNT) in your browser. + +#### Snowplow Analytics Debugger Chrome Extension + +Snowplow Analytics Debugger is a browser extension for testing frontend events. This works on production, staging, and local development environments. + +1. Install the [Snowplow Analytics Debugger](https://chrome.google.com/webstore/detail/snowplow-analytics-debugg/jbnlcgeengmijcghameodeaenefieedm) Chrome browser extension. +1. Open Chrome DevTools to the Snowplow Analytics Debugger tab. +1. Learn more at [Igloo Analytics](https://www.iglooanalytics.com/blog/snowplow-analytics-debugger-chrome-extension.html). + +#### Snowplow Inspector Chrome Extension + +Snowplow Inspector Chrome Extension is a browser extension for testing frontend events. This works on production, staging and local development environments. + +1. Install [Snowplow Inspector](https://chrome.google.com/webstore/detail/snowplow-inspector/maplkdomeamdlngconidoefjpogkmljm?hl=en). +1. Open the Chrome extension by pressing the Snowplow Inspector icon beside the address bar. +1. Click around on a webpage with Snowplow and you should see JavaScript events firing in the inspector window. + +### Snowplow Micro + +Snowplow Micro is a very small version of a full Snowplow data collection pipeline: small enough that it can be launched by a test suite. Events can be recorded into Snowplow Micro just as they can a full Snowplow pipeline. Micro then exposes an API that can be queried. + +Snowplow Micro is a Docker-based solution for testing frontend and backend events in a local development environment. You must modify GDK using the instructions below to set this up. + +- 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 <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. + +1. Install [Snowplow Micro](https://github.com/snowplow-incubator/snowplow-micro) by cloning the settings in [this project](https://gitlab.com/gitlab-org/snowplow-micro-configuration): +1. Navigate to the directory with the cloned project, and start the appropriate Docker + container with the following script: + + ```shell + ./snowplow-micro.sh + ``` + +1. Use GDK to start the PostgreSQL terminal and connect to the `gitlabhq_development` database: + + ```shell + gdk psql -d gitlabhq_development + ``` + +1. Update your instance's settings to enable Snowplow events and point to the Snowplow Micro collector: + + ```shell + update application_settings set snowplow_collector_hostname='localhost:9090', snowplow_enabled=true, snowplow_cookie_domain='.gitlab.com'; + ``` + +1. Update `DEFAULT_SNOWPLOW_OPTIONS` in `app/assets/javascripts/tracking/constants.js` to remove `forceSecureTracker: true`: + + ```diff + diff --git a/app/assets/javascripts/tracking/constants.js b/app/assets/javascripts/tracking/constants.js + index 598111e4086..eff38074d4c 100644 + --- a/app/assets/javascripts/tracking/constants.js + +++ b/app/assets/javascripts/tracking/constants.js + @@ -7,7 +7,6 @@ export const DEFAULT_SNOWPLOW_OPTIONS = { + appId: '', + userFingerprint: false, + respectDoNotTrack: true, + - forceSecureTracker: true, + eventMethod: 'post', + contexts: { webPage: true, performanceTiming: true }, + formTracking: false, + ``` + +1. Update `options` in `lib/gitlab/tracking.rb` to add `protocol` and `port`: + + ```diff + diff --git a/lib/gitlab/tracking.rb b/lib/gitlab/tracking.rb + index 618e359211b..e9084623c43 100644 + --- a/lib/gitlab/tracking.rb + +++ b/lib/gitlab/tracking.rb + @@ -41,7 +41,9 @@ def options(group) + cookie_domain: Gitlab::CurrentSettings.snowplow_cookie_domain, + app_id: Gitlab::CurrentSettings.snowplow_app_id, + form_tracking: additional_features, + - link_click_tracking: additional_features + + link_click_tracking: additional_features, + + protocol: 'http', + + port: 9090 + }.transform_keys! { |key| key.to_s.camelize(:lower).to_sym } + end + ``` + +1. Update `emitter` in `lib/gitlab/tracking/destinations/snowplow.rb` to change `protocol`: + + ```diff + diff --git a/lib/gitlab/tracking/destinations/snowplow.rb b/lib/gitlab/tracking/destinations/snowplow.rb + index 4fa844de325..5dd9d0eacfb 100644 + --- a/lib/gitlab/tracking/destinations/snowplow.rb + +++ b/lib/gitlab/tracking/destinations/snowplow.rb + @@ -40,7 +40,7 @@ def tracker + def emitter + SnowplowTracker::AsyncEmitter.new( + Gitlab::CurrentSettings.snowplow_collector_hostname, + - protocol: 'https' + + protocol: 'http' + ) + end + end + + ``` + +1. Restart GDK: + + ```shell + gdk restart + ``` + +1. Send a test Snowplow event from the Rails console: + + ```ruby + Gitlab::Tracking.event('category', 'action') + ``` + +1. Navigate to `localhost:9090/micro/good` to see the event. + +### Snowplow Mini + +[Snowplow Mini](https://github.com/snowplow/snowplow-mini) is an easily-deployable, single-instance version of Snowplow. + +Snowplow Mini can be used for testing frontend and backend events on a production, staging and local development environment. + +For GitLab.com, we're setting up a [QA and Testing environment](https://gitlab.com/gitlab-org/telemetry/-/issues/266) using Snowplow Mini. + +### Troubleshooting + +To control content security policy warnings when using an external host, you can allow or disallow them by modifying `config/gitlab.yml`. To allow them, add the relevant host for `connect_src`. For example, for `https://snowplow.trx.gitlab.net`: + +```yaml +development: + <<: *base + gitlab: + content_security_policy: + enabled: true + directives: + connect_src: "'self' http://localhost:* http://127.0.0.1:* ws://localhost:* wss://localhost:* ws://127.0.0.1:* https://snowplow.trx.gitlab.net/" +``` diff --git a/doc/development/snowplow/index.md b/doc/development/snowplow/index.md index a5c4938afb1..313a62ec6c8 100644 --- a/doc/development/snowplow/index.md +++ b/doc/development/snowplow/index.md @@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Snowplow Guide -This guide provides an overview of how Snowplow works, and implementation details. +This guide provides an overview of how Snowplow works. For more information about Product Intelligence, see: @@ -157,666 +157,10 @@ LIMIT 20 Snowplow JS adds [web-specific parameters](https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/snowplow-tracker-protocol/#Web-specific_parameters) to all web events by default. -## Snowplow JavaScript frontend tracking +## Implement Snowplow tracking -GitLab provides a `Tracking` interface that wraps the [Snowplow JavaScript tracker](https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/javascript-trackers/) to track custom events. For the recommended implementation type, see [Usage recommendations](#usage-recommendations). +See the [Implementation](implementation.md) guide. -Tracking implementations must have an `action` and a `category`. You can provide additional [structured event taxonomy](#structured-event-taxonomy) categories with an `extra` object that accepts key-value pairs. +## Snowplow schemas -| Field | Type | Default value | Description | -|:-----------|:-------|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `category` | string | `document.body.dataset.page` | Page or subsection of a page in which events are captured. | -| `action` | string | generic | Action the user is taking. Clicks must be `click` and activations must be `activate`. For example, focusing a form field is `activate_form_input`, and clicking a button is `click_button`. | -| `data` | object | `{}` | Additional data such as `label`, `property`, `value`, `context` as described in [Structured event taxonomy](#structured-event-taxonomy), and `extra` (key-value pairs object). | - -### Usage recommendations - -- Use [data attributes](#implement-data-attribute-tracking) on HTML elements that emit `click`, `show.bs.dropdown`, or `hide.bs.dropdown` events. -- Use the [Vue mixin](#implement-vue-component-tracking) for tracking custom events, or if the supported events for data attributes are not propagating. -- Use the [tracking class](#implement-raw-javascript-tracking) when tracking raw JavaScript files. - -### Implement data attribute tracking - -To implement tracking for HAML or Vue templates, add a [`data-track` attribute](#data-track-attributes) to the element. - -The following example shows `data-track-*` attributes assigned to a button: - -```haml -%button.btn{ data: { track: { action: "click_button", label: "template_preview", property: "my-template" } } } -``` - -```html -<button class="btn" - data-track-action="click_button" - data-track-label="template_preview" - data-track-property="my-template" - data-track-extra='{ "template_variant": "primary" }' -/> -``` - -#### `data-track` attributes - -| Attribute | Required | Description | -|:----------------------|:---------|:------------| -| `data-track-action` | true | Action the user is taking. Clicks must be prepended with `click` and activations must be prepended with `activate`. For example, focusing a form field is `activate_form_input` and clicking a button is `click_button`. Replaces `data-track-event`, which was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/290962) in GitLab 13.11. | -| `data-track-label` | false | The `label` as described in [structured event taxonomy](#structured-event-taxonomy). | -| `data-track-property` | false | The `property` as described in [structured event taxonomy](#structured-event-taxonomy). | -| `data-track-value` | false | The `value` as described in [structured event taxonomy](#structured-event-taxonomy). If omitted, this is the element's `value` property or `undefined`. For checkboxes, the default value is the element's checked attribute or `0` when unchecked. | -| `data-track-extra` | false | A key-value pairs object passed as a valid JSON string. This is added to the `extra` property in our [`gitlab_standard`](#gitlab_standard) schema. | -| `data-track-context` | false | The `context` as described in our [Structured event taxonomy](#structured-event-taxonomy). | - -#### Event listeners - -Event listeners are bound at the document level to handle click events in elements with data attributes. This allows them to be handled on re-rendering and changes to the DOM. Because of the way these events are bound, click events should not stop from propagating up the DOM tree. If click events are stopped from propagating, you must implement listeners and follow the instructions in [Implement Vue component tracking](#implement-vue-component-tracking) or [Implement raw JavaScript tracking](#implement-raw-javascript-tracking). - -#### Available helpers - -```ruby -tracking_attrs(label, action, property) # { data: { track_label... } } - -%button{ **tracking_attrs('main_navigation', 'click_button', 'navigation') } -``` - -#### Caveats - -When using the GitLab helper method [`nav_link`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/helpers/tab_helper.rb#L76) be sure to wrap `html_options` under the `html_options` keyword argument. -Be careful, as this behavior can be confused with the `ActionView` helper method [`link_to`](https://api.rubyonrails.org/v5.2.3/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to) that does not require additional wrapping of `html_options` - -```ruby -# Bad -= nav_link(controller: ['dashboard/groups', 'explore/groups'], data: { track_label: "explore_groups", track_action: "click_button" }) - -# Good -= nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { data: { track_label: "explore_groups", track_action: "click_button" } }) - -# Good (other helpers) -= link_to explore_groups_path, title: _("Explore"), data: { track_label: "explore_groups", track_action: "click_button" } -``` - -### Implement Vue component tracking - -For custom event tracking, use a Vue `mixin` in components. Vue `mixin` exposes the `Tracking.event` static method and the `track` method called from components or templates. You can specify tracking options in `data` or `computed`. These options override any defaults and allow the values to be dynamic from props or based on state. - -Default options are passed when an event is tracked from the component. If you don't specify an option, the default `document.body.dataset.page` is used. The default options are: - -- `category` -- `label` -- `property` -- `value` - -To implement Vue component tracking: - -1. Import the `Tracking` library and request a `mixin`: - - ```javascript - import Tracking from '~/tracking'; - const trackingMixin = Tracking.mixin; - ``` - -1. Provide categories to track the event from the component. For example, to track all events in a component with a label, use the `label` category: - - ```javascript - import Tracking from '~/tracking'; - const trackingMixin = Tracking.mixin({ label: 'right_sidebar' }); - ``` - -1. In the component, declare the Vue `mixin`. - - ```javascript - export default { - mixins: [trackingMixin], - // ...[component implementation]... - data() { - return { - expanded: false, - tracking: { - label: 'left_sidebar', - }, - }; - }, - }; - ``` - -1. To receive event data as a tracking object or computed property: - - Declare it in the `data` function. Use a `tracking` object when default event properties are dynamic or provided at runtime: - - ```javascript - export default { - name: 'RightSidebar', - mixins: [Tracking.mixin()], - data() { - return { - tracking: { - label: 'right_sidebar', - // category: '', - // property: '', - // value: '', - // experiment: '', - // extra: {}, - }, - }; - }, - }; - ``` - - - Declare it in the event data in the `track` function. This object merges with any previously provided options: - - ```javascript - this.track('click_button', { - label: 'right_sidebar', - }); - ``` - -1. Optional. Use the `track` method in a template: - - ```html - <template> - <div> - <button data-testid="toggle" @click="toggle">Toggle</button> - - <div v-if="expanded"> - <p>Hello world!</p> - <button @click="track('click_action')">Track another event</button> - </div> - </div> - </template> - ``` - -#### Implementation example - -```javascript -export default { - name: 'RightSidebar', - mixins: [Tracking.mixin({ label: 'right_sidebar' })], - data() { - return { - expanded: false, - }; - }, - methods: { - toggle() { - this.expanded = !this.expanded; - // Additional data will be merged, like `value` below - this.track('click_toggle', { value: Number(this.expanded) }); - } - } -}; -``` - -#### Testing example - -```javascript -import { mockTracking } from 'helpers/tracking_helper'; -// mockTracking(category, documentOverride, spyMethod) - -describe('RightSidebar.vue', () => { - let trackingSpy; - let wrapper; - - beforeEach(() => { - trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - }); - - const findToggle = () => wrapper.find('[data-testid="toggle"]'); - - it('tracks turning off toggle', () => { - findToggle().trigger('click'); - - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_toggle', { - label: 'right_sidebar', - value: 0, - }); - }); -}); -``` - -### Implement raw JavaScript tracking - -To call custom event tracking and instrumentation directly from the JavaScript file, call the `Tracking.event` static function. - -The following example demonstrates tracking a click on a button by manually calling `Tracking.event`. - -```javascript -import Tracking from '~/tracking'; - -const button = document.getElementById('create_from_template_button'); - -button.addEventListener('click', () => { - Tracking.event('dashboard:projects:index', 'click_button', { - label: 'create_from_template', - property: 'template_preview', - extra: { - templateVariant: 'primary', - valid: 1, - }, - }); -}); -``` - -#### Testing example - -```javascript -import Tracking from '~/tracking'; - -describe('MyTracking', () => { - let wrapper; - - beforeEach(() => { - jest.spyOn(Tracking, 'event'); - }); - - const findButton = () => wrapper.find('[data-testid="create_from_template"]'); - - it('tracks event', () => { - findButton().trigger('click'); - - expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', { - label: 'create_from_template', - property: 'template_preview', - extra: { - templateVariant: 'primary', - valid: true, - }, - }); - }); -}); -``` - -### Form tracking - -Enable Snowplow automatic [form tracking](https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/javascript-trackers/javascript-tracker/javascript-tracker-v2/tracking-specific-events/#form-tracking) by calling `Tracking.enableFormTracking` (after the DOM is ready) and providing a `config` object that includes at least one of the following elements: - -- `forms`: determines which forms are tracked, and are identified by the CSS class name. -- `fields`: determines which fields inside the tracked forms are tracked, and are identified by the field `name`. - -An optional list of contexts can be provided as the second argument. -Note that our [`gitlab_standard`](#gitlab_standard) schema is excluded from these events. - -```javascript -Tracking.enableFormTracking({ - forms: { allow: ['sign-in-form', 'password-recovery-form'] }, - fields: { allow: ['terms_and_conditions', 'newsletter_agreement'] }, -}); -``` - -#### Testing example - -```javascript -import Tracking from '~/tracking'; - -describe('MyFormTracking', () => { - let formTrackingSpy; - - beforeEach(() => { - formTrackingSpy = jest - .spyOn(Tracking, 'enableFormTracking') - .mockImplementation(() => null); - }); - - it('initialized with the correct configuration', () => { - expect(formTrackingSpy).toHaveBeenCalledWith({ - forms: { allow: ['sign-in-form', 'password-recovery-form'] }, - fields: { allow: ['terms_and_conditions', 'newsletter_agreement'] }, - }); - }); -}); -``` - -## Implement Snowplow Ruby (Backend) tracking - -GitLab provides `Gitlab::Tracking`, an interface that wraps the [Snowplow Ruby Tracker](https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/ruby-tracker/) for tracking custom events. - -Custom event tracking and instrumentation can be added by directly calling the `GitLab::Tracking.event` class method, which accepts the following arguments: - -| argument | type | default value | description | -|------------|---------------------------|---------------|-----------------------------------------------------------------------------------------------------------------------------------| -| `category` | String | | Area or aspect of the application. This could be `HealthCheckController` or `Lfs::FileTransformer` for instance. | -| `action` | String | | The action being taken, which can be anything from a controller action like `create` to something like an Active Record callback. | -| `label` | String | nil | As described in [Structured event taxonomy](#structured-event-taxonomy). | -| `property` | String | nil | As described in [Structured event taxonomy](#structured-event-taxonomy). | -| `value` | Numeric | nil | As described in [Structured event taxonomy](#structured-event-taxonomy). | -| `context` | Array\[SelfDescribingJSON\] | nil | An array of custom contexts to send with this event. Most events should not have any custom contexts. | -| `project` | Project | nil | The project associated with the event. | -| `user` | User | nil | The user associated with the event. | -| `namespace` | Namespace | nil | The namespace associated with the event. | -| `extra` | Hash | `{}` | Additional keyword arguments are collected into a hash and sent with the event. | - -Tracking can be viewed as either tracking user behavior, or can be used for instrumentation to monitor and visualize performance over time in an area or aspect of code. - -For example: - -```ruby -class Projects::CreateService < BaseService - def execute - project = Project.create(params) - - Gitlab::Tracking.event('Projects::CreateService', 'create_project', label: project.errors.full_messages.to_sentence, - property: project.valid?.to_s, project: project, user: current_user, namespace: namespace) - end -end -``` - -### Unit testing - -Use the `expect_snowplow_event` helper when testing backend Snowplow events. See [testing best practices]( -https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#test-snowplow-events) for details. - -### Performance - -We use the [AsyncEmitter](https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/ruby-tracker/emitters/#the-asyncemitter-class) when tracking events, which allows for instrumentation calls to be run in a background thread. This is still an active area of development. - -## Develop and test Snowplow - -There are several tools for developing and testing a Snowplow event. - -| Testing Tool | Frontend Tracking | Backend Tracking | Local Development Environment | Production Environment | Production Environment | -|----------------------------------------------|--------------------|---------------------|-------------------------------|------------------------|------------------------| -| Snowplow Analytics Debugger Chrome Extension | **{check-circle}** | **{dotted-circle}** | **{check-circle}** | **{check-circle}** | **{check-circle}** | -| Snowplow Inspector Chrome Extension | **{check-circle}** | **{dotted-circle}** | **{check-circle}** | **{check-circle}** | **{check-circle}** | -| Snowplow Micro | **{check-circle}** | **{check-circle}** | **{check-circle}** | **{dotted-circle}** | **{dotted-circle}** | -| Snowplow Mini | **{check-circle}** | **{check-circle}** | **{dotted-circle}** | **{status_preparing}** | **{status_preparing}** | - -**Legend** - -**{check-circle}** Available, **{status_preparing}** In progress, **{dotted-circle}** Not Planned - -### Test frontend events - -To test frontend events in development: - -- [Enable Snowplow tracking in the Admin Area](#enable-snowplow-tracking). -- Turn off any ad blockers that would prevent Snowplow JS from loading in your environment. -- Turn off "Do Not Track" (DNT) in your browser. - -#### Snowplow Analytics Debugger Chrome Extension - -Snowplow Analytics Debugger is a browser extension for testing frontend events. This works on production, staging, and local development environments. - -1. Install the [Snowplow Analytics Debugger](https://chrome.google.com/webstore/detail/snowplow-analytics-debugg/jbnlcgeengmijcghameodeaenefieedm) Chrome browser extension. -1. Open Chrome DevTools to the Snowplow Analytics Debugger tab. -1. Learn more at [Igloo Analytics](https://www.iglooanalytics.com/blog/snowplow-analytics-debugger-chrome-extension.html). - -#### Snowplow Inspector Chrome Extension - -Snowplow Inspector Chrome Extension is a browser extension for testing frontend events. This works on production, staging and local development environments. - -1. Install [Snowplow Inspector](https://chrome.google.com/webstore/detail/snowplow-inspector/maplkdomeamdlngconidoefjpogkmljm?hl=en). -1. Open the Chrome extension by pressing the Snowplow Inspector icon beside the address bar. -1. Click around on a webpage with Snowplow and you should see JavaScript events firing in the inspector window. - -### Snowplow Micro - -Snowplow Micro is a very small version of a full Snowplow data collection pipeline: small enough that it can be launched by a test suite. Events can be recorded into Snowplow Micro just as they can a full Snowplow pipeline. Micro then exposes an API that can be queried. - -Snowplow Micro is a Docker-based solution for testing frontend and backend events in a local development environment. You must modify GDK using the instructions below to set this up. - -- 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 <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. - -1. Install [Snowplow Micro](https://github.com/snowplow-incubator/snowplow-micro) by cloning the settings in [this project](https://gitlab.com/gitlab-org/snowplow-micro-configuration): -1. Navigate to the directory with the cloned project, and start the appropriate Docker - container with the following script: - - ```shell - ./snowplow-micro.sh - ``` - -1. Use GDK to start the PostgreSQL terminal and connect to the `gitlabhq_development` database: - - ```shell - gdk psql -d gitlabhq_development - ``` - -1. Update your instance's settings to enable Snowplow events and point to the Snowplow Micro collector: - - ```shell - update application_settings set snowplow_collector_hostname='localhost:9090', snowplow_enabled=true, snowplow_cookie_domain='.gitlab.com'; - ``` - -1. Update `DEFAULT_SNOWPLOW_OPTIONS` in `app/assets/javascripts/tracking/constants.js` to remove `forceSecureTracker: true`: - - ```diff - diff --git a/app/assets/javascripts/tracking/constants.js b/app/assets/javascripts/tracking/constants.js - index 598111e4086..eff38074d4c 100644 - --- a/app/assets/javascripts/tracking/constants.js - +++ b/app/assets/javascripts/tracking/constants.js - @@ -7,7 +7,6 @@ export const DEFAULT_SNOWPLOW_OPTIONS = { - appId: '', - userFingerprint: false, - respectDoNotTrack: true, - - forceSecureTracker: true, - eventMethod: 'post', - contexts: { webPage: true, performanceTiming: true }, - formTracking: false, - ``` - -1. Update `options` in `lib/gitlab/tracking.rb` to add `protocol` and `port`: - - ```diff - diff --git a/lib/gitlab/tracking.rb b/lib/gitlab/tracking.rb - index 618e359211b..e9084623c43 100644 - --- a/lib/gitlab/tracking.rb - +++ b/lib/gitlab/tracking.rb - @@ -41,7 +41,9 @@ def options(group) - cookie_domain: Gitlab::CurrentSettings.snowplow_cookie_domain, - app_id: Gitlab::CurrentSettings.snowplow_app_id, - form_tracking: additional_features, - - link_click_tracking: additional_features - + link_click_tracking: additional_features, - + protocol: 'http', - + port: 9090 - }.transform_keys! { |key| key.to_s.camelize(:lower).to_sym } - end - ``` - -1. Update `emitter` in `lib/gitlab/tracking/destinations/snowplow.rb` to change `protocol`: - - ```diff - diff --git a/lib/gitlab/tracking/destinations/snowplow.rb b/lib/gitlab/tracking/destinations/snowplow.rb - index 4fa844de325..5dd9d0eacfb 100644 - --- a/lib/gitlab/tracking/destinations/snowplow.rb - +++ b/lib/gitlab/tracking/destinations/snowplow.rb - @@ -40,7 +40,7 @@ def tracker - def emitter - SnowplowTracker::AsyncEmitter.new( - Gitlab::CurrentSettings.snowplow_collector_hostname, - - protocol: 'https' - + protocol: 'http' - ) - end - end - - ``` - -1. Restart GDK: - - ```shell - gdk restart - ``` - -1. Send a test Snowplow event from the Rails console: - - ```ruby - Gitlab::Tracking.event('category', 'action') - ``` - -1. Navigate to `localhost:9090/micro/good` to see the event. - -### Snowplow Mini - -[Snowplow Mini](https://github.com/snowplow/snowplow-mini) is an easily-deployable, single-instance version of Snowplow. - -Snowplow Mini can be used for testing frontend and backend events on a production, staging and local development environment. - -For GitLab.com, we're setting up a [QA and Testing environment](https://gitlab.com/gitlab-org/telemetry/-/issues/266) using Snowplow Mini. - -### Troubleshooting - -To control content security policy warnings when using an external host, you can allow or disallow them by modifying `config/gitlab.yml`. To allow them, add the relevant host for `connect_src`. For example, for `https://snowplow.trx.gitlab.net`: - -```yaml -development: - <<: *base - gitlab: - content_security_policy: - enabled: true - directives: - connect_src: "'self' http://localhost:* http://127.0.0.1:* ws://localhost:* wss://localhost:* ws://127.0.0.1:* https://snowplow.trx.gitlab.net/" -``` - -## Snowplow Schemas - -### `gitlab_standard` - -We are including the [`gitlab_standard` schema](https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_standard/jsonschema/) with every event. See [Standardize Snowplow Schema](https://gitlab.com/groups/gitlab-org/-/epics/5218) for details. - -The [`StandardContext`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/tracking/standard_context.rb) class represents this schema in the application. - -| Field Name | Required | Type | Description | -|----------------|---------------------|-----------------------|---------------------------------------------------------------------------------------------| -| `project_id` | **{dotted-circle}** | integer | | -| `namespace_id` | **{dotted-circle}** | integer | | -| `environment` | **{check-circle}** | string (max 32 chars) | Name of the source environment, such as `production` or `staging` | -| `source` | **{check-circle}** | string (max 32 chars) | Name of the source application, such as `gitlab-rails` or `gitlab-javascript` | -| `plan` | **{dotted-circle}** | string (max 32 chars) | Name of the plan for the namespace, such as `free`, `premium`, or `ultimate`. Automatically picked from the `namespace`. | -| `extra` | **{dotted-circle}** | JSON | Any additional data associated with the event, in the form of key-value pairs | - -### Default Schema - -| Field Name | Required | Type | Description | -|--------------------------|---------------------|-----------|----------------------------------------------------------------------------------------------------------------------------------| -| `app_id` | **{check-circle}** | string | Unique identifier for website / application | -| `base_currency` | **{dotted-circle}** | string | Reporting currency | -| `br_colordepth` | **{dotted-circle}** | integer | Browser color depth | -| `br_cookies` | **{dotted-circle}** | boolean | Does the browser permit cookies? | -| `br_family` | **{dotted-circle}** | string | Browser family | -| `br_features_director` | **{dotted-circle}** | boolean | Director plugin installed? | -| `br_features_flash` | **{dotted-circle}** | boolean | Flash plugin installed? | -| `br_features_gears` | **{dotted-circle}** | boolean | Google gears installed? | -| `br_features_java` | **{dotted-circle}** | boolean | Java plugin installed? | -| `br_features_pdf` | **{dotted-circle}** | boolean | Adobe PDF plugin installed? | -| `br_features_quicktime` | **{dotted-circle}** | boolean | Quicktime plugin installed? | -| `br_features_realplayer` | **{dotted-circle}** | boolean | RealPlayer plugin installed? | -| `br_features_silverlight` | **{dotted-circle}** | boolean | Silverlight plugin installed? | -| `br_features_windowsmedia` | **{dotted-circle}** | boolean | Windows media plugin installed? | -| `br_lang` | **{dotted-circle}** | string | Language the browser is set to | -| `br_name` | **{dotted-circle}** | string | Browser name | -| `br_renderengine` | **{dotted-circle}** | string | Browser rendering engine | -| `br_type` | **{dotted-circle}** | string | Browser type | -| `br_version` | **{dotted-circle}** | string | Browser version | -| `br_viewheight` | **{dotted-circle}** | string | Browser viewport height | -| `br_viewwidth` | **{dotted-circle}** | string | Browser viewport width | -| `collector_tstamp` | **{dotted-circle}** | timestamp | Time stamp for the event recorded by the collector | -| `contexts` | **{dotted-circle}** | | | -| `derived_contexts` | **{dotted-circle}** | | Contexts derived in the Enrich process | -| `derived_tstamp` | **{dotted-circle}** | timestamp | Timestamp making allowance for inaccurate device clock | -| `doc_charset` | **{dotted-circle}** | string | Web page's character encoding | -| `doc_height` | **{dotted-circle}** | string | Web page height | -| `doc_width` | **{dotted-circle}** | string | Web page width | -| `domain_sessionid` | **{dotted-circle}** | string | Unique identifier (UUID) for this visit of this user_id to this domain | -| `domain_sessionidx` | **{dotted-circle}** | integer | Index of number of visits that this user_id has made to this domain (The first visit is `1`) | -| `domain_userid` | **{dotted-circle}** | string | Unique identifier for a user, based on a first party cookie (so domain specific) | -| `dvce_created_tstamp` | **{dotted-circle}** | timestamp | Timestamp when event occurred, as recorded by client device | -| `dvce_ismobile` | **{dotted-circle}** | boolean | Indicates whether device is mobile | -| `dvce_screenheight` | **{dotted-circle}** | string | Screen / monitor resolution | -| `dvce_screenwidth` | **{dotted-circle}** | string | Screen / monitor resolution | -| `dvce_sent_tstamp` | **{dotted-circle}** | timestamp | Timestamp when event was sent by client device to collector | -| `dvce_type` | **{dotted-circle}** | string | Type of device | -| `etl_tags` | **{dotted-circle}** | string | JSON of tags for this ETL run | -| `etl_tstamp` | **{dotted-circle}** | timestamp | Timestamp event began ETL | -| `event` | **{dotted-circle}** | string | Event type | -| `event_fingerprint` | **{dotted-circle}** | string | Hash client-set event fields | -| `event_format` | **{dotted-circle}** | string | Format for event | -| `event_id` | **{dotted-circle}** | string | Event UUID | -| `event_name` | **{dotted-circle}** | string | Event name | -| `event_vendor` | **{dotted-circle}** | string | The company who developed the event model | -| `event_version` | **{dotted-circle}** | string | Version of event schema | -| `geo_city` | **{dotted-circle}** | string | City of IP origin | -| `geo_country` | **{dotted-circle}** | string | Country of IP origin | -| `geo_latitude` | **{dotted-circle}** | string | An approximate latitude | -| `geo_longitude` | **{dotted-circle}** | string | An approximate longitude | -| `geo_region` | **{dotted-circle}** | string | Region of IP origin | -| `geo_region_name` | **{dotted-circle}** | string | Region of IP origin | -| `geo_timezone` | **{dotted-circle}** | string | Time zone of IP origin | -| `geo_zipcode` | **{dotted-circle}** | string | Zip (postal) code of IP origin | -| `ip_domain` | **{dotted-circle}** | string | Second level domain name associated with the visitor's IP address | -| `ip_isp` | **{dotted-circle}** | string | Visitor's ISP | -| `ip_netspeed` | **{dotted-circle}** | string | Visitor's connection type | -| `ip_organization` | **{dotted-circle}** | string | Organization associated with the visitor's IP address – defaults to ISP name if none is found | -| `mkt_campaign` | **{dotted-circle}** | string | The campaign ID | -| `mkt_clickid` | **{dotted-circle}** | string | The click ID | -| `mkt_content` | **{dotted-circle}** | string | The content or ID of the ad. | -| `mkt_medium` | **{dotted-circle}** | string | Type of traffic source | -| `mkt_network` | **{dotted-circle}** | string | The ad network to which the click ID belongs | -| `mkt_source` | **{dotted-circle}** | string | The company / website where the traffic came from | -| `mkt_term` | **{dotted-circle}** | string | Keywords associated with the referrer | -| `name_tracker` | **{dotted-circle}** | string | The tracker namespace | -| `network_userid` | **{dotted-circle}** | string | Unique identifier for a user, based on a cookie from the collector (so set at a network level and shouldn't be set by a tracker) | -| `os_family` | **{dotted-circle}** | string | Operating system family | -| `os_manufacturer` | **{dotted-circle}** | string | Manufacturers of operating system | -| `os_name` | **{dotted-circle}** | string | Name of operating system | -| `os_timezone` | **{dotted-circle}** | string | Client operating system time zone | -| `page_referrer` | **{dotted-circle}** | string | Referrer URL | -| `page_title` | **{dotted-circle}** | string | Page title | -| `page_url` | **{dotted-circle}** | string | Page URL | -| `page_urlfragment` | **{dotted-circle}** | string | Fragment aka anchor | -| `page_urlhost` | **{dotted-circle}** | string | Host aka domain | -| `page_urlpath` | **{dotted-circle}** | string | Path to page | -| `page_urlport` | **{dotted-circle}** | integer | Port if specified, 80 if not | -| `page_urlquery` | **{dotted-circle}** | string | Query string | -| `page_urlscheme` | **{dotted-circle}** | string | Scheme (protocol name) | -| `platform` | **{dotted-circle}** | string | The platform the app runs on | -| `pp_xoffset_max` | **{dotted-circle}** | integer | Maximum page x offset seen in the last ping period | -| `pp_xoffset_min` | **{dotted-circle}** | integer | Minimum page x offset seen in the last ping period | -| `pp_yoffset_max` | **{dotted-circle}** | integer | Maximum page y offset seen in the last ping period | -| `pp_yoffset_min` | **{dotted-circle}** | integer | Minimum page y offset seen in the last ping period | -| `refr_domain_userid` | **{dotted-circle}** | string | The Snowplow `domain_userid` of the referring website | -| `refr_dvce_tstamp` | **{dotted-circle}** | timestamp | The time of attaching the `domain_userid` to the inbound link | -| `refr_medium` | **{dotted-circle}** | string | Type of referer | -| `refr_source` | **{dotted-circle}** | string | Name of referer if recognised | -| `refr_term` | **{dotted-circle}** | string | Keywords if source is a search engine | -| `refr_urlfragment` | **{dotted-circle}** | string | Referer URL fragment | -| `refr_urlhost` | **{dotted-circle}** | string | Referer host | -| `refr_urlpath` | **{dotted-circle}** | string | Referer page path | -| `refr_urlport` | **{dotted-circle}** | integer | Referer port | -| `refr_urlquery` | **{dotted-circle}** | string | Referer URL query string | -| `refr_urlscheme` | **{dotted-circle}** | string | Referer scheme | -| `se_action` | **{dotted-circle}** | string | The action / event itself | -| `se_category` | **{dotted-circle}** | string | The category of event | -| `se_label` | **{dotted-circle}** | string | A label often used to refer to the 'object' the action is performed on | -| `se_property` | **{dotted-circle}** | string | A property associated with either the action or the object | -| `se_value` | **{dotted-circle}** | decimal | A value associated with the user action | -| `ti_category` | **{dotted-circle}** | string | Item category | -| `ti_currency` | **{dotted-circle}** | string | Currency | -| `ti_name` | **{dotted-circle}** | string | Item name | -| `ti_orderid` | **{dotted-circle}** | string | Order ID | -| `ti_price` | **{dotted-circle}** | decimal | Item price | -| `ti_price_base` | **{dotted-circle}** | decimal | Item price in base currency | -| `ti_quantity` | **{dotted-circle}** | integer | Item quantity | -| `ti_sku` | **{dotted-circle}** | string | Item SKU | -| `tr_affiliation` | **{dotted-circle}** | string | Transaction affiliation (such as channel) | -| `tr_city` | **{dotted-circle}** | string | Delivery address: city | -| `tr_country` | **{dotted-circle}** | string | Delivery address: country | -| `tr_currency` | **{dotted-circle}** | string | Transaction Currency | -| `tr_orderid` | **{dotted-circle}** | string | Order ID | -| `tr_shipping` | **{dotted-circle}** | decimal | Delivery cost charged | -| `tr_shipping_base` | **{dotted-circle}** | decimal | Shipping cost in base currency | -| `tr_state` | **{dotted-circle}** | string | Delivery address: state | -| `tr_tax` | **{dotted-circle}** | decimal | Transaction tax value (such as amount of VAT included) | -| `tr_tax_base` | **{dotted-circle}** | decimal | Tax applied in base currency | -| `tr_total` | **{dotted-circle}** | decimal | Transaction total value | -| `tr_total_base` | **{dotted-circle}** | decimal | Total amount of transaction in base currency | -| `true_tstamp` | **{dotted-circle}** | timestamp | User-set exact timestamp | -| `txn_id` | **{dotted-circle}** | string | Transaction ID | -| `unstruct_event` | **{dotted-circle}** | JSON | The properties of the event | -| `uploaded_at` | **{dotted-circle}** | | | -| `user_fingerprint` | **{dotted-circle}** | integer | User identifier based on (hopefully unique) browser features | -| `user_id` | **{dotted-circle}** | string | Unique identifier for user, set by the business using setUserId | -| `user_ipaddress` | **{dotted-circle}** | string | IP address | -| `useragent` | **{dotted-circle}** | string | User agent (expressed as a browser string) | -| `v_collector` | **{dotted-circle}** | string | Collector version | -| `v_etl` | **{dotted-circle}** | string | ETL version | -| `v_tracker` | **{dotted-circle}** | string | Identifier for Snowplow tracker | +See the [Snowplow schemas](schemas.md) guide. diff --git a/doc/development/snowplow/review_guidelines.md b/doc/development/snowplow/review_guidelines.md index fa0985f6943..77e95ef8c16 100644 --- a/doc/development/snowplow/review_guidelines.md +++ b/doc/development/snowplow/review_guidelines.md @@ -26,18 +26,18 @@ events or touches Snowplow related files. #### The merge request **author** should - For frontend events, when relevant, add a screenshot of the event in - the [testing tool](../snowplow/index.md#develop-and-test-snowplow) used. + the [testing tool](implementation.md#develop-and-test-snowplow) used. - For backend events, when relevant, add the output of the - [Snowplow Micro](index.md#snowplow-mini) good events + [Snowplow Micro](implementation.md#snowplow-mini) good events `GET http://localhost:9090/micro/good` (it might be a good idea to reset with `GET http://localhost:9090/micro/reset` first). - Update the [Event Dictionary](event_dictionary_guide.md). #### The Product Intelligence **reviewer** should -- Check that the [event taxonomy](../snowplow/index.md#structured-event-taxonomy) is correct. -- Check the [usage recommendations](../snowplow/index.md#usage-recommendations). +- Check that the [event taxonomy](index.md#structured-event-taxonomy) is correct. +- Check the [usage recommendations](implementation.md#usage-recommendations). - Check that the [Event Dictionary](event_dictionary_guide.md) is up-to-date. - If needed, check that the events are firing locally using one of the -[testing tools](../snowplow/index.md#develop-and-test-snowplow) available. +[testing tools](implementation.md#develop-and-test-snowplow) available. - Approve the MR, and relabel the MR with `~"product intelligence::approved"`. diff --git a/doc/development/snowplow/schemas.md b/doc/development/snowplow/schemas.md new file mode 100644 index 00000000000..d5ecfd806f3 --- /dev/null +++ b/doc/development/snowplow/schemas.md @@ -0,0 +1,161 @@ +--- +stage: Growth +group: Product Intelligence +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 +--- + +# Snowplow schemas + +This page provides Snowplow schema reference for GitLab events. + +## `gitlab_standard` + +We are including the [`gitlab_standard` schema](https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_standard/jsonschema/) with every event. See [Standardize Snowplow Schema](https://gitlab.com/groups/gitlab-org/-/epics/5218) for details. + +The [`StandardContext`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/tracking/standard_context.rb) class represents this schema in the application. + +| Field Name | Required | Type | Description | +|----------------|---------------------|-----------------------|---------------------------------------------------------------------------------------------| +| `project_id` | **{dotted-circle}** | integer | | +| `namespace_id` | **{dotted-circle}** | integer | | +| `environment` | **{check-circle}** | string (max 32 chars) | Name of the source environment, such as `production` or `staging` | +| `source` | **{check-circle}** | string (max 32 chars) | Name of the source application, such as `gitlab-rails` or `gitlab-javascript` | +| `plan` | **{dotted-circle}** | string (max 32 chars) | Name of the plan for the namespace, such as `free`, `premium`, or `ultimate`. Automatically picked from the `namespace`. | +| `extra` | **{dotted-circle}** | JSON | Any additional data associated with the event, in the form of key-value pairs | + +## Default Schema + +| Field Name | Required | Type | Description | +|--------------------------|---------------------|-----------|----------------------------------------------------------------------------------------------------------------------------------| +| `app_id` | **{check-circle}** | string | Unique identifier for website / application | +| `base_currency` | **{dotted-circle}** | string | Reporting currency | +| `br_colordepth` | **{dotted-circle}** | integer | Browser color depth | +| `br_cookies` | **{dotted-circle}** | boolean | Does the browser permit cookies? | +| `br_family` | **{dotted-circle}** | string | Browser family | +| `br_features_director` | **{dotted-circle}** | boolean | Director plugin installed? | +| `br_features_flash` | **{dotted-circle}** | boolean | Flash plugin installed? | +| `br_features_gears` | **{dotted-circle}** | boolean | Google gears installed? | +| `br_features_java` | **{dotted-circle}** | boolean | Java plugin installed? | +| `br_features_pdf` | **{dotted-circle}** | boolean | Adobe PDF plugin installed? | +| `br_features_quicktime` | **{dotted-circle}** | boolean | Quicktime plugin installed? | +| `br_features_realplayer` | **{dotted-circle}** | boolean | RealPlayer plugin installed? | +| `br_features_silverlight` | **{dotted-circle}** | boolean | Silverlight plugin installed? | +| `br_features_windowsmedia` | **{dotted-circle}** | boolean | Windows media plugin installed? | +| `br_lang` | **{dotted-circle}** | string | Language the browser is set to | +| `br_name` | **{dotted-circle}** | string | Browser name | +| `br_renderengine` | **{dotted-circle}** | string | Browser rendering engine | +| `br_type` | **{dotted-circle}** | string | Browser type | +| `br_version` | **{dotted-circle}** | string | Browser version | +| `br_viewheight` | **{dotted-circle}** | string | Browser viewport height | +| `br_viewwidth` | **{dotted-circle}** | string | Browser viewport width | +| `collector_tstamp` | **{dotted-circle}** | timestamp | Time stamp for the event recorded by the collector | +| `contexts` | **{dotted-circle}** | | | +| `derived_contexts` | **{dotted-circle}** | | Contexts derived in the Enrich process | +| `derived_tstamp` | **{dotted-circle}** | timestamp | Timestamp making allowance for inaccurate device clock | +| `doc_charset` | **{dotted-circle}** | string | Web page's character encoding | +| `doc_height` | **{dotted-circle}** | string | Web page height | +| `doc_width` | **{dotted-circle}** | string | Web page width | +| `domain_sessionid` | **{dotted-circle}** | string | Unique identifier (UUID) for this visit of this user_id to this domain | +| `domain_sessionidx` | **{dotted-circle}** | integer | Index of number of visits that this user_id has made to this domain (The first visit is `1`) | +| `domain_userid` | **{dotted-circle}** | string | Unique identifier for a user, based on a first party cookie (so domain specific) | +| `dvce_created_tstamp` | **{dotted-circle}** | timestamp | Timestamp when event occurred, as recorded by client device | +| `dvce_ismobile` | **{dotted-circle}** | boolean | Indicates whether device is mobile | +| `dvce_screenheight` | **{dotted-circle}** | string | Screen / monitor resolution | +| `dvce_screenwidth` | **{dotted-circle}** | string | Screen / monitor resolution | +| `dvce_sent_tstamp` | **{dotted-circle}** | timestamp | Timestamp when event was sent by client device to collector | +| `dvce_type` | **{dotted-circle}** | string | Type of device | +| `etl_tags` | **{dotted-circle}** | string | JSON of tags for this ETL run | +| `etl_tstamp` | **{dotted-circle}** | timestamp | Timestamp event began ETL | +| `event` | **{dotted-circle}** | string | Event type | +| `event_fingerprint` | **{dotted-circle}** | string | Hash client-set event fields | +| `event_format` | **{dotted-circle}** | string | Format for event | +| `event_id` | **{dotted-circle}** | string | Event UUID | +| `event_name` | **{dotted-circle}** | string | Event name | +| `event_vendor` | **{dotted-circle}** | string | The company who developed the event model | +| `event_version` | **{dotted-circle}** | string | Version of event schema | +| `geo_city` | **{dotted-circle}** | string | City of IP origin | +| `geo_country` | **{dotted-circle}** | string | Country of IP origin | +| `geo_latitude` | **{dotted-circle}** | string | An approximate latitude | +| `geo_longitude` | **{dotted-circle}** | string | An approximate longitude | +| `geo_region` | **{dotted-circle}** | string | Region of IP origin | +| `geo_region_name` | **{dotted-circle}** | string | Region of IP origin | +| `geo_timezone` | **{dotted-circle}** | string | Time zone of IP origin | +| `geo_zipcode` | **{dotted-circle}** | string | Zip (postal) code of IP origin | +| `ip_domain` | **{dotted-circle}** | string | Second level domain name associated with the visitor's IP address | +| `ip_isp` | **{dotted-circle}** | string | Visitor's ISP | +| `ip_netspeed` | **{dotted-circle}** | string | Visitor's connection type | +| `ip_organization` | **{dotted-circle}** | string | Organization associated with the visitor's IP address – defaults to ISP name if none is found | +| `mkt_campaign` | **{dotted-circle}** | string | The campaign ID | +| `mkt_clickid` | **{dotted-circle}** | string | The click ID | +| `mkt_content` | **{dotted-circle}** | string | The content or ID of the ad. | +| `mkt_medium` | **{dotted-circle}** | string | Type of traffic source | +| `mkt_network` | **{dotted-circle}** | string | The ad network to which the click ID belongs | +| `mkt_source` | **{dotted-circle}** | string | The company / website where the traffic came from | +| `mkt_term` | **{dotted-circle}** | string | Keywords associated with the referrer | +| `name_tracker` | **{dotted-circle}** | string | The tracker namespace | +| `network_userid` | **{dotted-circle}** | string | Unique identifier for a user, based on a cookie from the collector (so set at a network level and shouldn't be set by a tracker) | +| `os_family` | **{dotted-circle}** | string | Operating system family | +| `os_manufacturer` | **{dotted-circle}** | string | Manufacturers of operating system | +| `os_name` | **{dotted-circle}** | string | Name of operating system | +| `os_timezone` | **{dotted-circle}** | string | Client operating system time zone | +| `page_referrer` | **{dotted-circle}** | string | Referrer URL | +| `page_title` | **{dotted-circle}** | string | Page title | +| `page_url` | **{dotted-circle}** | string | Page URL | +| `page_urlfragment` | **{dotted-circle}** | string | Fragment aka anchor | +| `page_urlhost` | **{dotted-circle}** | string | Host aka domain | +| `page_urlpath` | **{dotted-circle}** | string | Path to page | +| `page_urlport` | **{dotted-circle}** | integer | Port if specified, 80 if not | +| `page_urlquery` | **{dotted-circle}** | string | Query string | +| `page_urlscheme` | **{dotted-circle}** | string | Scheme (protocol name) | +| `platform` | **{dotted-circle}** | string | The platform the app runs on | +| `pp_xoffset_max` | **{dotted-circle}** | integer | Maximum page x offset seen in the last ping period | +| `pp_xoffset_min` | **{dotted-circle}** | integer | Minimum page x offset seen in the last ping period | +| `pp_yoffset_max` | **{dotted-circle}** | integer | Maximum page y offset seen in the last ping period | +| `pp_yoffset_min` | **{dotted-circle}** | integer | Minimum page y offset seen in the last ping period | +| `refr_domain_userid` | **{dotted-circle}** | string | The Snowplow `domain_userid` of the referring website | +| `refr_dvce_tstamp` | **{dotted-circle}** | timestamp | The time of attaching the `domain_userid` to the inbound link | +| `refr_medium` | **{dotted-circle}** | string | Type of referer | +| `refr_source` | **{dotted-circle}** | string | Name of referer if recognised | +| `refr_term` | **{dotted-circle}** | string | Keywords if source is a search engine | +| `refr_urlfragment` | **{dotted-circle}** | string | Referer URL fragment | +| `refr_urlhost` | **{dotted-circle}** | string | Referer host | +| `refr_urlpath` | **{dotted-circle}** | string | Referer page path | +| `refr_urlport` | **{dotted-circle}** | integer | Referer port | +| `refr_urlquery` | **{dotted-circle}** | string | Referer URL query string | +| `refr_urlscheme` | **{dotted-circle}** | string | Referer scheme | +| `se_action` | **{dotted-circle}** | string | The action / event itself | +| `se_category` | **{dotted-circle}** | string | The category of event | +| `se_label` | **{dotted-circle}** | string | A label often used to refer to the 'object' the action is performed on | +| `se_property` | **{dotted-circle}** | string | A property associated with either the action or the object | +| `se_value` | **{dotted-circle}** | decimal | A value associated with the user action | +| `ti_category` | **{dotted-circle}** | string | Item category | +| `ti_currency` | **{dotted-circle}** | string | Currency | +| `ti_name` | **{dotted-circle}** | string | Item name | +| `ti_orderid` | **{dotted-circle}** | string | Order ID | +| `ti_price` | **{dotted-circle}** | decimal | Item price | +| `ti_price_base` | **{dotted-circle}** | decimal | Item price in base currency | +| `ti_quantity` | **{dotted-circle}** | integer | Item quantity | +| `ti_sku` | **{dotted-circle}** | string | Item SKU | +| `tr_affiliation` | **{dotted-circle}** | string | Transaction affiliation (such as channel) | +| `tr_city` | **{dotted-circle}** | string | Delivery address: city | +| `tr_country` | **{dotted-circle}** | string | Delivery address: country | +| `tr_currency` | **{dotted-circle}** | string | Transaction Currency | +| `tr_orderid` | **{dotted-circle}** | string | Order ID | +| `tr_shipping` | **{dotted-circle}** | decimal | Delivery cost charged | +| `tr_shipping_base` | **{dotted-circle}** | decimal | Shipping cost in base currency | +| `tr_state` | **{dotted-circle}** | string | Delivery address: state | +| `tr_tax` | **{dotted-circle}** | decimal | Transaction tax value (such as amount of VAT included) | +| `tr_tax_base` | **{dotted-circle}** | decimal | Tax applied in base currency | +| `tr_total` | **{dotted-circle}** | decimal | Transaction total value | +| `tr_total_base` | **{dotted-circle}** | decimal | Total amount of transaction in base currency | +| `true_tstamp` | **{dotted-circle}** | timestamp | User-set exact timestamp | +| `txn_id` | **{dotted-circle}** | string | Transaction ID | +| `unstruct_event` | **{dotted-circle}** | JSON | The properties of the event | +| `uploaded_at` | **{dotted-circle}** | | | +| `user_fingerprint` | **{dotted-circle}** | integer | User identifier based on (hopefully unique) browser features | +| `user_id` | **{dotted-circle}** | string | Unique identifier for user, set by the business using setUserId | +| `user_ipaddress` | **{dotted-circle}** | string | IP address | +| `useragent` | **{dotted-circle}** | string | User agent (expressed as a browser string) | +| `v_collector` | **{dotted-circle}** | string | Collector version | +| `v_etl` | **{dotted-circle}** | string | ETL version | +| `v_tracker` | **{dotted-circle}** | string | Identifier for Snowplow tracker | diff --git a/doc/user/analytics/index.md b/doc/user/analytics/index.md index 5b7e6e39187..c01788f4fc4 100644 --- a/doc/user/analytics/index.md +++ b/doc/user/analytics/index.md @@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w When we describe GitLab analytics, we use the following terms: -- **Cycle time:** The duration of only the execution work alone. Often displayed in combination with "lead time," which is longer. GitLab measures cycle time from issue first merge request creation to issue close. This approach underestimates lead time because merge request creation is always later than commit time. GitLab displays cycle time in [group-level Value Stream Analytics](../group/value_stream_analytics/index.md). +- **Cycle time:** The duration of only the execution work. Cycle time is often displayed in combination with the lead time, which is longer than the cycle time. GitLab measures cycle time from the earliest commit of a [linked issue's merge request](../project/issues/crosslinking_issues.md) to when that issue is closed. The cycle time approach underestimates the lead time because merge request creation is always later than commit time. GitLab displays cycle time in [group-level Value Stream Analytics](../group/value_stream_analytics/index.md) and [project-level Value Stream Analytics](../analytics/value_stream_analytics.md). - **Deploys:** The total number of successful deployments to production in the given time frame (across all applicable projects). GitLab displays deploys in [group-level Value Stream Analytics](../group/value_stream_analytics/index.md) and [project-level Value Stream Analytics](value_stream_analytics.md). - **DORA (DevOps Research and Assessment)** ["Four Keys"](https://cloud.google.com/blog/products/devops-sre/using-the-four-keys-to-measure-your-devops-performance): - **Speed/Velocity** diff --git a/lib/gitlab/popen/runner.rb b/lib/gitlab/popen/runner.rb index cd9ad270cd8..60c2082844c 100644 --- a/lib/gitlab/popen/runner.rb +++ b/lib/gitlab/popen/runner.rb @@ -31,7 +31,7 @@ module Gitlab end def all_stderr_empty? - results.all? { |result| result.stderr.empty? } + results.all? { |result| stderr_empty_ignoring_spring(result) } end def failed_results @@ -40,9 +40,22 @@ module Gitlab def warned_results results.select do |result| - result.status.success? && !result.stderr.empty? + result.status.success? && !stderr_empty_ignoring_spring(result) end end + + private + + # NOTE: This is sometimes required instead of just calling `result.stderr.empty?`, if we + # want to ignore the spring "Running via Spring preloader..." output to STDERR. + # The `Spring.quiet=true` method which spring supports doesn't work, because it doesn't + # work to make it quiet when using spring binstubs (the STDERR is printed by `bin/spring` + # itself when first required, so there's no opportunity to set Spring.quiet=true). + # This should probably be opened as a bug against Spring, with a pull request to support a + # `SPRING_QUIET` env var as well. + def stderr_empty_ignoring_spring(result) + result.stderr.empty? || result.stderr =~ /\ARunning via Spring preloader in process [0-9]+\Z/ + end end end end diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake index 62d31803f6e..7d1721f1df8 100644 --- a/lib/tasks/lint.rake +++ b/lib/tasks/lint.rake @@ -12,13 +12,6 @@ unless Rails.env.production? dev:load ] do Gitlab::Utils::Override.verify! - end - - desc "GitLab | Lint | Static verification with database" - task static_verification_with_database: %w[ - lint:static_verification_env - dev:load - ] do Gitlab::Utils::DelegatorOverride.verify! end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 557df5c0ded..eaef6cf77e7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8896,10 +8896,10 @@ msgstr "" msgid "ContainerRegistry|Delete selected tags" msgstr "" -msgid "ContainerRegistry|Deleting the image repository will delete all images and tags inside. This action cannot be undone. Please type the following to confirm: %{code}" +msgid "ContainerRegistry|Delete tag" msgstr "" -msgid "ContainerRegistry|Deletion disabled due to missing or insufficient permissions." +msgid "ContainerRegistry|Deleting the image repository will delete all images and tags inside. This action cannot be undone. Please type the following to confirm: %{code}" msgstr "" msgid "ContainerRegistry|Digest: %{imageId}" @@ -29241,9 +29241,6 @@ msgstr "" msgid "Runners are processes that pick up and execute CI/CD jobs for GitLab." msgstr "" -msgid "Runners can be:" -msgstr "" - msgid "Runners currently online: %{active_runners_count}" msgstr "" @@ -37400,7 +37397,7 @@ msgstr "" msgid "ValueStreamAnalytics|Median time from issue created to issue closed." msgstr "" -msgid "ValueStreamAnalytics|Median time from issue first merge request created to issue closed." +msgid "ValueStreamAnalytics|Median time from the earliest commit of a linked issue's merge request to when that issue is closed." msgstr "" msgid "ValueStreamAnalytics|Number of commits pushed to the default branch" @@ -38863,9 +38860,6 @@ msgstr "" msgid "You can recover this project until %{date}" msgstr "" -msgid "You can register runners as separate users, on separate servers, and on your local machine. Register as many runners as you want." -msgstr "" - msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}" msgstr "" @@ -39751,6 +39745,9 @@ msgstr "" msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}" msgstr "" +msgid "ciReport|%{prefix} %{strongStart}%{score}%{strongEnd} %{delta} %{deltaPercent} in %{path}" +msgstr "" + msgid "ciReport|%{remainingPackagesCount} more" msgstr "" @@ -39790,6 +39787,11 @@ msgstr "" msgid "ciReport|Browser performance test metrics: " msgstr "" +msgid "ciReport|Browser performance test metrics: %{strongStart}%{changesFound}%{strongEnd} change" +msgid_plural "ciReport|Browser performance test metrics: %{strongStart}%{changesFound}%{strongEnd} changes" +msgstr[0] "" +msgstr[1] "" + msgid "ciReport|Browser performance test metrics: No changes" msgstr "" @@ -39865,6 +39867,9 @@ msgstr "" msgid "ciReport|Investigate this vulnerability by creating an issue" msgstr "" +msgid "ciReport|Load Performance" +msgstr "" + msgid "ciReport|Load performance test metrics detected %{strongStart}%{changesFound}%{strongEnd} change" msgid_plural "ciReport|Load performance test metrics detected %{strongStart}%{changesFound}%{strongEnd} changes" msgstr[0] "" diff --git a/qa/qa/page/project/registry/show.rb b/qa/qa/page/project/registry/show.rb index 03c547fc8b5..f2472a83401 100644 --- a/qa/qa/page/project/registry/show.rb +++ b/qa/qa/page/project/registry/show.rb @@ -10,6 +10,10 @@ module QA end view 'app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue' do + element :more_actions_menu + end + + view 'app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue' do element :tag_delete_button end @@ -30,6 +34,7 @@ module QA end def click_delete + click_element(:more_actions_menu) click_element(:tag_delete_button) find_button('Delete').click end diff --git a/spec/controllers/every_controller_spec.rb b/spec/controllers/every_controller_spec.rb index 54bbb5dbd01..902872b6e92 100644 --- a/spec/controllers/every_controller_spec.rb +++ b/spec/controllers/every_controller_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe "Every controller" do context "feature categories" do let_it_be(:feature_categories) do - YAML.load_file(Rails.root.join('config', 'feature_categories.yml')).map(&:to_sym).to_set + Gitlab::FeatureCategories.default.categories.map(&:to_sym).to_set end let_it_be(:controller_actions) do diff --git a/spec/features/groups/container_registry_spec.rb b/spec/features/groups/container_registry_spec.rb index 65374263f45..be694374d62 100644 --- a/spec/features/groups/container_registry_spec.rb +++ b/spec/features/groups/container_registry_spec.rb @@ -81,6 +81,7 @@ RSpec.describe 'Container Registry', :js do expect(service).to receive(:execute).with(container_repository) { { status: :success } } expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(container_repository.project, user, tags: ['latest']) { service } + first('[data-testid="additional-actions"]').click first('[data-testid="single-delete-button"]').click expect(find('.modal .modal-title')).to have_content _('Remove tag') find('.modal .modal-footer .btn-danger').click diff --git a/spec/features/projects/container_registry_spec.rb b/spec/features/projects/container_registry_spec.rb index 40d0260eafd..facee24f656 100644 --- a/spec/features/projects/container_registry_spec.rb +++ b/spec/features/projects/container_registry_spec.rb @@ -96,6 +96,7 @@ RSpec.describe 'Container Registry', :js do expect(service).to receive(:execute).with(container_repository) { { status: :success } } expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(container_repository.project, user, tags: ['1']) { service } + first('[data-testid="additional-actions"]').click first('[data-testid="single-delete-button"]').click expect(find('.modal .modal-title')).to have_content _('Remove tag') find('.modal .modal-footer .btn-danger').click diff --git a/spec/frontend/commit/commit_pipeline_status_component_spec.js b/spec/frontend/commit/commit_pipeline_status_component_spec.js index 8082b8524e7..3a549e66eb7 100644 --- a/spec/frontend/commit/commit_pipeline_status_component_spec.js +++ b/spec/frontend/commit/commit_pipeline_status_component_spec.js @@ -1,7 +1,7 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Visibility from 'visibilityjs'; -import { getJSONFixture } from 'helpers/fixtures'; +import fixture from 'test_fixtures/pipelines/pipelines.json'; import createFlash from '~/flash'; import Poll from '~/lib/utils/poll'; import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; @@ -20,7 +20,7 @@ jest.mock('~/projects/tree/services/commit_pipeline_service', () => describe('Commit pipeline status component', () => { let wrapper; - const { pipelines } = getJSONFixture('pipelines/pipelines.json'); + const { pipelines } = fixture; const { status: mockCiStatus } = pipelines[0].details; const defaultProps = { diff --git a/spec/frontend/commit/pipelines/pipelines_table_spec.js b/spec/frontend/commit/pipelines/pipelines_table_spec.js index 1defb3d586c..17f7be9d1d7 100644 --- a/spec/frontend/commit/pipelines/pipelines_table_spec.js +++ b/spec/frontend/commit/pipelines/pipelines_table_spec.js @@ -1,6 +1,7 @@ import { GlEmptyState, GlLoadingIcon, GlModal, GlTable } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import fixture from 'test_fixtures/pipelines/pipelines.json'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import Api from '~/api'; @@ -8,7 +9,6 @@ import PipelinesTable from '~/commit/pipelines/pipelines_table.vue'; import axios from '~/lib/utils/axios_utils'; describe('Pipelines table in Commits and Merge requests', () => { - const jsonFixtureName = 'pipelines/pipelines.json'; let wrapper; let pipeline; let mock; @@ -37,7 +37,7 @@ describe('Pipelines table in Commits and Merge requests', () => { beforeEach(() => { mock = new MockAdapter(axios); - const { pipelines } = getJSONFixture(jsonFixtureName); + const { pipelines } = fixture; pipeline = pipelines.find((p) => p.user !== null && p.commit !== null); }); diff --git a/spec/frontend/monitoring/fixture_data.js b/spec/frontend/monitoring/fixture_data.js index d20a111c701..6a19815883a 100644 --- a/spec/frontend/monitoring/fixture_data.js +++ b/spec/frontend/monitoring/fixture_data.js @@ -1,3 +1,4 @@ +import fixture from 'test_fixtures/metrics_dashboard/environment_metrics_dashboard.json'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { metricStates } from '~/monitoring/constants'; import { mapToDashboardViewModel } from '~/monitoring/stores/utils'; @@ -5,10 +6,7 @@ import { stateAndPropsFromDataset } from '~/monitoring/utils'; import { metricsResult } from './mock_data'; -// Use globally available `getJSONFixture` so this file can be imported by both karma and jest specs -export const metricsDashboardResponse = getJSONFixture( - 'metrics_dashboard/environment_metrics_dashboard.json', -); +export const metricsDashboardResponse = fixture; export const metricsDashboardPayload = metricsDashboardResponse.dashboard; diff --git a/spec/frontend/notebook/cells/code_spec.js b/spec/frontend/notebook/cells/code_spec.js index e14767f2594..669bdc2f89a 100644 --- a/spec/frontend/notebook/cells/code_spec.js +++ b/spec/frontend/notebook/cells/code_spec.js @@ -1,14 +1,17 @@ import Vue from 'vue'; +import fixture from 'test_fixtures/blob/notebook/basic.json'; import CodeComponent from '~/notebook/cells/code.vue'; const Component = Vue.extend(CodeComponent); describe('Code component', () => { let vm; + let json; beforeEach(() => { - json = getJSONFixture('blob/notebook/basic.json'); + // Clone fixture as it could be modified by tests + json = JSON.parse(JSON.stringify(fixture)); }); const setupComponent = (cell) => { diff --git a/spec/frontend/notebook/cells/markdown_spec.js b/spec/frontend/notebook/cells/markdown_spec.js index 707efa21528..36b1e91f15f 100644 --- a/spec/frontend/notebook/cells/markdown_spec.js +++ b/spec/frontend/notebook/cells/markdown_spec.js @@ -1,6 +1,9 @@ import { mount } from '@vue/test-utils'; import katex from 'katex'; import Vue from 'vue'; +import markdownTableJson from 'test_fixtures/blob/notebook/markdown-table.json'; +import basicJson from 'test_fixtures/blob/notebook/basic.json'; +import mathJson from 'test_fixtures/blob/notebook/math.json'; import MarkdownComponent from '~/notebook/cells/markdown.vue'; const Component = Vue.extend(MarkdownComponent); @@ -35,7 +38,7 @@ describe('Markdown component', () => { let json; beforeEach(() => { - json = getJSONFixture('blob/notebook/basic.json'); + json = basicJson; // eslint-disable-next-line prefer-destructuring cell = json.cells[1]; @@ -104,7 +107,7 @@ describe('Markdown component', () => { describe('tables', () => { beforeEach(() => { - json = getJSONFixture('blob/notebook/markdown-table.json'); + json = markdownTableJson; }); it('renders images and text', () => { @@ -135,7 +138,7 @@ describe('Markdown component', () => { describe('katex', () => { beforeEach(() => { - json = getJSONFixture('blob/notebook/math.json'); + json = mathJson; }); it('renders multi-line katex', async () => { diff --git a/spec/frontend/notebook/cells/output/index_spec.js b/spec/frontend/notebook/cells/output/index_spec.js index 2985abf0f4f..7ece73d375c 100644 --- a/spec/frontend/notebook/cells/output/index_spec.js +++ b/spec/frontend/notebook/cells/output/index_spec.js @@ -1,11 +1,11 @@ import Vue from 'vue'; +import json from 'test_fixtures/blob/notebook/basic.json'; import CodeComponent from '~/notebook/cells/output/index.vue'; const Component = Vue.extend(CodeComponent); describe('Output component', () => { let vm; - let json; const createComponent = (output) => { vm = new Component({ @@ -17,11 +17,6 @@ describe('Output component', () => { vm.$mount(); }; - beforeEach(() => { - // This is the output after rendering a jupyter notebook - json = getJSONFixture('blob/notebook/basic.json'); - }); - describe('text output', () => { beforeEach((done) => { const textType = json.cells[2]; diff --git a/spec/frontend/notebook/index_spec.js b/spec/frontend/notebook/index_spec.js index 4d0dacaf37e..cd531d628b3 100644 --- a/spec/frontend/notebook/index_spec.js +++ b/spec/frontend/notebook/index_spec.js @@ -1,18 +1,13 @@ import { mount } from '@vue/test-utils'; import Vue from 'vue'; +import json from 'test_fixtures/blob/notebook/basic.json'; +import jsonWithWorksheet from 'test_fixtures/blob/notebook/worksheets.json'; import Notebook from '~/notebook/index.vue'; const Component = Vue.extend(Notebook); describe('Notebook component', () => { let vm; - let json; - let jsonWithWorksheet; - - beforeEach(() => { - json = getJSONFixture('blob/notebook/basic.json'); - jsonWithWorksheet = getJSONFixture('blob/notebook/worksheets.json'); - }); function buildComponent(notebook) { return mount(Component, { diff --git a/spec/frontend/notes/components/diff_with_note_spec.js b/spec/frontend/notes/components/diff_with_note_spec.js index e997fc4da50..c352265654b 100644 --- a/spec/frontend/notes/components/diff_with_note_spec.js +++ b/spec/frontend/notes/components/diff_with_note_spec.js @@ -1,10 +1,9 @@ import { shallowMount } from '@vue/test-utils'; +import discussionFixture from 'test_fixtures/merge_requests/diff_discussion.json'; +import imageDiscussionFixture from 'test_fixtures/merge_requests/image_diff_discussion.json'; import { createStore } from '~/mr_notes/stores'; import DiffWithNote from '~/notes/components/diff_with_note.vue'; -const discussionFixture = 'merge_requests/diff_discussion.json'; -const imageDiscussionFixture = 'merge_requests/image_diff_discussion.json'; - describe('diff_with_note', () => { let store; let wrapper; @@ -35,7 +34,7 @@ describe('diff_with_note', () => { describe('text diff', () => { beforeEach(() => { - const diffDiscussion = getJSONFixture(discussionFixture)[0]; + const diffDiscussion = discussionFixture[0]; wrapper = shallowMount(DiffWithNote, { propsData: { @@ -75,7 +74,7 @@ describe('diff_with_note', () => { describe('image diff', () => { beforeEach(() => { - const imageDiscussion = getJSONFixture(imageDiscussionFixture)[0]; + const imageDiscussion = imageDiscussionFixture[0]; wrapper = shallowMount(DiffWithNote, { propsData: { discussion: imageDiscussion, diffFile: {} }, store, diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js index a364a524e7b..727ef02dcbb 100644 --- a/spec/frontend/notes/components/noteable_discussion_spec.js +++ b/spec/frontend/notes/components/noteable_discussion_spec.js @@ -1,5 +1,6 @@ import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; +import discussionWithTwoUnresolvedNotes from 'test_fixtures/merge_requests/resolved_diff_discussion.json'; import { trimText } from 'helpers/text_helper'; import mockDiffFile from 'jest/diffs/mock_data/diff_file'; import DiscussionNotes from '~/notes/components/discussion_notes.vue'; @@ -17,8 +18,6 @@ import { userDataMock, } from '../mock_data'; -const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json'; - describe('noteable_discussion component', () => { let store; let wrapper; @@ -119,7 +118,7 @@ describe('noteable_discussion component', () => { describe('for resolved thread', () => { beforeEach(() => { - const discussion = getJSONFixture(discussionWithTwoUnresolvedNotes)[0]; + const discussion = discussionWithTwoUnresolvedNotes[0]; wrapper.setProps({ discussion }); }); @@ -133,7 +132,7 @@ describe('noteable_discussion component', () => { describe('for unresolved thread', () => { beforeEach(() => { const discussion = { - ...getJSONFixture(discussionWithTwoUnresolvedNotes)[0], + ...discussionWithTwoUnresolvedNotes[0], expanded: true, }; discussion.resolved = false; diff --git a/spec/frontend/notes/stores/getters_spec.js b/spec/frontend/notes/stores/getters_spec.js index 3adb5da020e..9a11fdba508 100644 --- a/spec/frontend/notes/stores/getters_spec.js +++ b/spec/frontend/notes/stores/getters_spec.js @@ -1,3 +1,4 @@ +import discussionWithTwoUnresolvedNotes from 'test_fixtures/merge_requests/resolved_diff_discussion.json'; import { DESC, ASC } from '~/notes/constants'; import * as getters from '~/notes/stores/getters'; import { @@ -17,8 +18,6 @@ import { draftDiffDiscussion, } from '../mock_data'; -const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json'; - // Helper function to ensure that we're using the same schema across tests. const createDiscussionNeighborParams = (discussionId, diffOrder, step) => ({ discussionId, @@ -123,7 +122,7 @@ describe('Getters Notes Store', () => { describe('resolvedDiscussionsById', () => { it('ignores unresolved system notes', () => { - const [discussion] = getJSONFixture(discussionWithTwoUnresolvedNotes); + const [discussion] = discussionWithTwoUnresolvedNotes; discussion.notes[0].resolved = true; discussion.notes[1].resolved = false; state.discussions.push(discussion); diff --git a/spec/frontend/pipelines/components/pipelines_list/pipeline_mini_graph_spec.js b/spec/frontend/pipelines/components/pipelines_list/pipeline_mini_graph_spec.js index 154828aff4b..1cb43c199aa 100644 --- a/spec/frontend/pipelines/components/pipelines_list/pipeline_mini_graph_spec.js +++ b/spec/frontend/pipelines/components/pipelines_list/pipeline_mini_graph_spec.js @@ -1,8 +1,8 @@ import { shallowMount } from '@vue/test-utils'; +import { pipelines } from 'test_fixtures/pipelines/pipelines.json'; import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue'; import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue'; -const { pipelines } = getJSONFixture('pipelines/pipelines.json'); const mockStages = pipelines[0].details.stages; describe('Pipeline Mini Graph', () => { diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js index aa30062c987..2875498bb52 100644 --- a/spec/frontend/pipelines/pipelines_spec.js +++ b/spec/frontend/pipelines/pipelines_spec.js @@ -4,6 +4,7 @@ import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { chunk } from 'lodash'; import { nextTick } from 'vue'; +import mockPipelinesResponse from 'test_fixtures/pipelines/pipelines.json'; import setWindowLocation from 'helpers/set_window_location_helper'; import { TEST_HOST } from 'helpers/test_constants'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; @@ -33,7 +34,6 @@ jest.mock('~/experimentation/utils', () => ({ const mockProjectPath = 'twitter/flight'; const mockProjectId = '21'; const mockPipelinesEndpoint = `/${mockProjectPath}/pipelines.json`; -const mockPipelinesResponse = getJSONFixture('pipelines/pipelines.json'); const mockPipelinesIds = mockPipelinesResponse.pipelines.map(({ id }) => id); const mockPipelineWithStages = mockPipelinesResponse.pipelines.find( (p) => p.details.stages && p.details.stages.length, diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js index 4472a5ae70d..fb019b463b1 100644 --- a/spec/frontend/pipelines/pipelines_table_spec.js +++ b/spec/frontend/pipelines/pipelines_table_spec.js @@ -1,6 +1,7 @@ import '~/commons'; import { GlTable } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; +import fixture from 'test_fixtures/pipelines/pipelines.json'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue'; import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue'; @@ -20,8 +21,6 @@ describe('Pipelines Table', () => { let pipeline; let wrapper; - const jsonFixtureName = 'pipelines/pipelines.json'; - const defaultProps = { pipelines: [], viewType: 'root', @@ -29,7 +28,8 @@ describe('Pipelines Table', () => { }; const createMockPipeline = () => { - const { pipelines } = getJSONFixture(jsonFixtureName); + // Clone fixture as it could be modified by tests + const { pipelines } = JSON.parse(JSON.stringify(fixture)); return pipelines.find((p) => p.user !== null && p.commit !== null); }; diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js index e931ddb8496..84a9f4776b9 100644 --- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js +++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js @@ -1,5 +1,5 @@ import MockAdapter from 'axios-mock-adapter'; -import { getJSONFixture } from 'helpers/fixtures'; +import testReports from 'test_fixtures/pipelines/test_report.json'; import { TEST_HOST } from 'helpers/test_constants'; import testAction from 'helpers/vuex_action_helper'; import createFlash from '~/flash'; @@ -13,7 +13,6 @@ describe('Actions TestReports Store', () => { let mock; let state; - const testReports = getJSONFixture('pipelines/test_report.json'); const summary = { total_count: 1 }; const suiteEndpoint = `${TEST_HOST}/tests/suite.json`; diff --git a/spec/frontend/pipelines/test_reports/stores/getters_spec.js b/spec/frontend/pipelines/test_reports/stores/getters_spec.js index f8298fdaba5..70e3a01dbf1 100644 --- a/spec/frontend/pipelines/test_reports/stores/getters_spec.js +++ b/spec/frontend/pipelines/test_reports/stores/getters_spec.js @@ -1,4 +1,4 @@ -import { getJSONFixture } from 'helpers/fixtures'; +import testReports from 'test_fixtures/pipelines/test_report.json'; import * as getters from '~/pipelines/stores/test_reports/getters'; import { iconForTestStatus, @@ -9,8 +9,6 @@ import { describe('Getters TestReports Store', () => { let state; - const testReports = getJSONFixture('pipelines/test_report.json'); - const defaultState = { blobPath: '/test/blob/path', testReports, diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js index 191e9e7391c..f2dbeec6a06 100644 --- a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js +++ b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js @@ -1,12 +1,10 @@ -import { getJSONFixture } from 'helpers/fixtures'; +import testReports from 'test_fixtures/pipelines/test_report.json'; import * as types from '~/pipelines/stores/test_reports/mutation_types'; import mutations from '~/pipelines/stores/test_reports/mutations'; describe('Mutations TestReports Store', () => { let mockState; - const testReports = getJSONFixture('pipelines/test_report.json'); - const defaultState = { endpoint: '', testReports: {}, diff --git a/spec/frontend/pipelines/test_reports/test_reports_spec.js b/spec/frontend/pipelines/test_reports/test_reports_spec.js index e44d59ba888..384b7cf6930 100644 --- a/spec/frontend/pipelines/test_reports/test_reports_spec.js +++ b/spec/frontend/pipelines/test_reports/test_reports_spec.js @@ -1,7 +1,7 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; -import { getJSONFixture } from 'helpers/fixtures'; +import testReports from 'test_fixtures/pipelines/test_report.json'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import EmptyState from '~/pipelines/components/test_reports/empty_state.vue'; import TestReports from '~/pipelines/components/test_reports/test_reports.vue'; @@ -16,8 +16,6 @@ describe('Test reports app', () => { let wrapper; let store; - const testReports = getJSONFixture('pipelines/test_report.json'); - const loadingSpinner = () => wrapper.findComponent(GlLoadingIcon); const testsDetail = () => wrapper.findByTestId('tests-detail'); const emptyState = () => wrapper.findComponent(EmptyState); diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js index a87145cc557..793bad6b82a 100644 --- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js +++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js @@ -1,7 +1,7 @@ import { GlButton, GlFriendlyWrap, GlLink, GlPagination } from '@gitlab/ui'; import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; -import { getJSONFixture } from 'helpers/fixtures'; +import testReports from 'test_fixtures/pipelines/test_report.json'; import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue'; import { TestStatus } from '~/pipelines/constants'; import * as getters from '~/pipelines/stores/test_reports/getters'; @@ -17,7 +17,7 @@ describe('Test reports suite table', () => { const { test_suites: [testSuite], - } = getJSONFixture('pipelines/test_report.json'); + } = testReports; testSuite.test_cases = [...testSuite.test_cases, ...skippedTestCases]; const testCases = testSuite.test_cases; diff --git a/spec/frontend/pipelines/test_reports/test_summary_spec.js b/spec/frontend/pipelines/test_reports/test_summary_spec.js index df404d87c99..7eed6671fb9 100644 --- a/spec/frontend/pipelines/test_reports/test_summary_spec.js +++ b/spec/frontend/pipelines/test_reports/test_summary_spec.js @@ -1,5 +1,5 @@ import { mount } from '@vue/test-utils'; -import { getJSONFixture } from 'helpers/fixtures'; +import testReports from 'test_fixtures/pipelines/test_report.json'; import Summary from '~/pipelines/components/test_reports/test_summary.vue'; import { formattedTime } from '~/pipelines/stores/test_reports/utils'; @@ -8,7 +8,7 @@ describe('Test reports summary', () => { const { test_suites: [testSuite], - } = getJSONFixture('pipelines/test_report.json'); + } = testReports; const backButton = () => wrapper.find('.js-back-button'); const totalTests = () => wrapper.find('.js-total-tests'); diff --git a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js index 892a3742fea..0813739d72f 100644 --- a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js +++ b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js @@ -1,6 +1,6 @@ import { mount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; -import { getJSONFixture } from 'helpers/fixtures'; +import testReports from 'test_fixtures/pipelines/test_report.json'; import SummaryTable from '~/pipelines/components/test_reports/test_summary_table.vue'; import * as getters from '~/pipelines/stores/test_reports/getters'; @@ -11,8 +11,6 @@ describe('Test reports summary table', () => { let wrapper; let store; - const testReports = getJSONFixture('pipelines/test_report.json'); - const allSuitesRows = () => wrapper.findAll('.js-suite-row'); const noSuitesToShow = () => wrapper.find('.js-no-tests-suites'); diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js index c8fcb3116cd..7716443a42a 100644 --- a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js +++ b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js @@ -1,13 +1,12 @@ -import { GlFormCheckbox, GlSprintf, GlIcon } from '@gitlab/ui'; +import { GlFormCheckbox, GlSprintf, GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import DeleteButton from '~/registry/explorer/components/delete_button.vue'; + import component from '~/registry/explorer/components/details_page/tags_list_row.vue'; import { REMOVE_TAG_BUTTON_TITLE, - REMOVE_TAG_BUTTON_DISABLE_TOOLTIP, MISSING_MANIFEST_WARNING_TOOLTIP, NOT_AVAILABLE_TEXT, NOT_AVAILABLE_SIZE, @@ -25,19 +24,20 @@ describe('tags list row', () => { const defaultProps = { tag, isMobile: false, index: 0 }; - const findCheckbox = () => wrapper.find(GlFormCheckbox); + const findCheckbox = () => wrapper.findComponent(GlFormCheckbox); const findName = () => wrapper.find('[data-testid="name"]'); const findSize = () => wrapper.find('[data-testid="size"]'); const findTime = () => wrapper.find('[data-testid="time"]'); const findShortRevision = () => wrapper.find('[data-testid="digest"]'); - const findClipboardButton = () => wrapper.find(ClipboardButton); - const findDeleteButton = () => wrapper.find(DeleteButton); - const findTimeAgoTooltip = () => wrapper.find(TimeAgoTooltip); + const findClipboardButton = () => wrapper.findComponent(ClipboardButton); + const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip); const findDetailsRows = () => wrapper.findAll(DetailsRow); const findPublishedDateDetail = () => wrapper.find('[data-testid="published-date-detail"]'); const findManifestDetail = () => wrapper.find('[data-testid="manifest-detail"]'); const findConfigurationDetail = () => wrapper.find('[data-testid="configuration-detail"]'); - const findWarningIcon = () => wrapper.find(GlIcon); + const findWarningIcon = () => wrapper.findComponent(GlIcon); + const findAdditionalActionsMenu = () => wrapper.findComponent(GlDropdown); + const findDeleteButton = () => wrapper.findComponent(GlDropdownItem); const mountComponent = (propsData = defaultProps) => { wrapper = shallowMount(component, { @@ -45,6 +45,7 @@ describe('tags list row', () => { GlSprintf, ListItem, DetailsRow, + GlDropdown, }, propsData, directives: { @@ -262,44 +263,59 @@ describe('tags list row', () => { }); }); - describe('delete button', () => { + describe('additional actions menu', () => { it('exists', () => { mountComponent(); - expect(findDeleteButton().exists()).toBe(true); + expect(findAdditionalActionsMenu().exists()).toBe(true); }); - it('has the correct props/attributes', () => { + it('has the correct props', () => { mountComponent(); - expect(findDeleteButton().attributes()).toMatchObject({ - title: REMOVE_TAG_BUTTON_TITLE, - tooltiptitle: REMOVE_TAG_BUTTON_DISABLE_TOOLTIP, - tooltipdisabled: 'true', + expect(findAdditionalActionsMenu().props()).toMatchObject({ + icon: 'ellipsis_v', + text: 'More actions', + textSrOnly: true, + category: 'tertiary', + right: true, }); }); it.each` - canDelete | digest | disabled - ${true} | ${null} | ${true} - ${false} | ${'foo'} | ${true} - ${false} | ${null} | ${true} - ${true} | ${'foo'} | ${true} + canDelete | digest | disabled | visible + ${true} | ${null} | ${true} | ${false} + ${false} | ${'foo'} | ${true} | ${false} + ${false} | ${null} | ${true} | ${false} + ${true} | ${'foo'} | ${true} | ${false} + ${true} | ${'foo'} | ${false} | ${true} `( - 'is disabled when canDelete is $canDelete and digest is $digest and disabled is $disabled', - ({ canDelete, digest, disabled }) => { + 'is $visible that is visible when canDelete is $canDelete and digest is $digest and disabled is $disabled', + ({ canDelete, digest, disabled, visible }) => { mountComponent({ ...defaultProps, tag: { ...tag, canDelete, digest }, disabled }); - expect(findDeleteButton().attributes('disabled')).toBe('true'); + expect(findAdditionalActionsMenu().exists()).toBe(visible); }, ); - it('delete event emits delete', () => { - mountComponent(); + describe('delete button', () => { + it('exists and has the correct attrs', () => { + mountComponent(); + + expect(findDeleteButton().exists()).toBe(true); + expect(findDeleteButton().attributes()).toMatchObject({ + variant: 'danger', + }); + expect(findDeleteButton().text()).toBe(REMOVE_TAG_BUTTON_TITLE); + }); - findDeleteButton().vm.$emit('delete'); + it('delete event emits delete', () => { + mountComponent(); - expect(wrapper.emitted('delete')).toEqual([[]]); + findDeleteButton().vm.$emit('click'); + + expect(wrapper.emitted('delete')).toEqual([[]]); + }); }); }); diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js index ba332e0f918..33e9c122080 100644 --- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js +++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js @@ -14,7 +14,6 @@ import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_ import RunnerList from '~/runner/components/runner_list.vue'; import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue'; import RunnerPagination from '~/runner/components/runner_pagination.vue'; -import RunnerTypeHelp from '~/runner/components/runner_type_help.vue'; import { ADMIN_FILTERED_SEARCH_NAMESPACE, @@ -51,7 +50,6 @@ describe('AdminRunnersApp', () => { let wrapper; let mockRunnersQuery; - const findRunnerTypeHelp = () => wrapper.findComponent(RunnerTypeHelp); const findRunnerManualSetupHelp = () => wrapper.findComponent(RunnerManualSetupHelp); const findRunnerList = () => wrapper.findComponent(RunnerList); const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination)); @@ -88,10 +86,6 @@ describe('AdminRunnersApp', () => { wrapper.destroy(); }); - it('shows the runner type help', () => { - expect(findRunnerTypeHelp().exists()).toBe(true); - }); - it('shows the runner setup instructions', () => { expect(findRunnerManualSetupHelp().props('registrationToken')).toBe(mockRegistrationToken); }); diff --git a/spec/frontend/runner/components/runner_type_help_spec.js b/spec/frontend/runner/components/runner_type_help_spec.js deleted file mode 100644 index f0d03282f8e..00000000000 --- a/spec/frontend/runner/components/runner_type_help_spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import { GlBadge } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import RunnerTypeHelp from '~/runner/components/runner_type_help.vue'; - -describe('RunnerTypeHelp', () => { - let wrapper; - - const findBadges = () => wrapper.findAllComponents(GlBadge); - - const createComponent = () => { - wrapper = mount(RunnerTypeHelp); - }; - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('Displays each of the runner types', () => { - expect(findBadges().at(0).text()).toBe('shared'); - expect(findBadges().at(1).text()).toBe('group'); - expect(findBadges().at(2).text()).toBe('specific'); - }); - - it('Displays runner states', () => { - expect(findBadges().at(3).text()).toBe('locked'); - expect(findBadges().at(4).text()).toBe('paused'); - }); -}); diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js index aeab297c8f4..5f3aabd4bc3 100644 --- a/spec/frontend/runner/group_runners/group_runners_app_spec.js +++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js @@ -13,7 +13,6 @@ import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_ import RunnerList from '~/runner/components/runner_list.vue'; import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue'; import RunnerPagination from '~/runner/components/runner_pagination.vue'; -import RunnerTypeHelp from '~/runner/components/runner_type_help.vue'; import { CREATED_ASC, @@ -49,7 +48,6 @@ describe('GroupRunnersApp', () => { let wrapper; let mockGroupRunnersQuery; - const findRunnerTypeHelp = () => wrapper.findComponent(RunnerTypeHelp); const findRunnerManualSetupHelp = () => wrapper.findComponent(RunnerManualSetupHelp); const findRunnerList = () => wrapper.findComponent(RunnerList); const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination)); @@ -83,10 +81,6 @@ describe('GroupRunnersApp', () => { await waitForPromises(); }); - it('shows the runner type help', () => { - expect(findRunnerTypeHelp().exists()).toBe(true); - }); - it('shows the runner setup instructions', () => { expect(findRunnerManualSetupHelp().props('registrationToken')).toBe(mockRegistrationToken); }); diff --git a/spec/frontend/sidebar/todo_spec.js b/spec/frontend/sidebar/todo_spec.js index ff6da3abad0..6829e688c65 100644 --- a/spec/frontend/sidebar/todo_spec.js +++ b/spec/frontend/sidebar/todo_spec.js @@ -27,7 +27,7 @@ describe('SidebarTodo', () => { it.each` state | classes ${false} | ${['gl-button', 'btn', 'btn-default', 'btn-todo', 'issuable-header-btn', 'float-right']} - ${true} | ${['btn-blank', 'btn-todo', 'sidebar-collapsed-icon', 'dont-change-state']} + ${true} | ${['btn-blank', 'btn-todo', 'sidebar-collapsed-icon', 'js-dont-change-state']} `('returns todo button classes for when `collapsed` prop is `$state`', ({ state, classes }) => { createComponent({ collapsed: state }); expect(wrapper.find('button').classes()).toStrictEqual(classes); diff --git a/spec/lib/api/every_api_endpoint_spec.rb b/spec/lib/api/every_api_endpoint_spec.rb index a9f325edefd..5fe14823a29 100644 --- a/spec/lib/api/every_api_endpoint_spec.rb +++ b/spec/lib/api/every_api_endpoint_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe 'Every API endpoint' do context 'feature categories' do let_it_be(:feature_categories) do - YAML.load_file(Rails.root.join('config', 'feature_categories.yml')).map(&:to_sym).to_set + Gitlab::FeatureCategories.default.categories.map(&:to_sym).to_set end let_it_be(:api_endpoints) do diff --git a/spec/lib/gitlab/etag_caching/router/graphql_spec.rb b/spec/lib/gitlab/etag_caching/router/graphql_spec.rb index d151dcba413..9a6787e3640 100644 --- a/spec/lib/gitlab/etag_caching/router/graphql_spec.rb +++ b/spec/lib/gitlab/etag_caching/router/graphql_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Gitlab::EtagCaching::Router::Graphql do end it 'has a valid feature category for every route', :aggregate_failures do - feature_categories = YAML.load_file(Rails.root.join('config', 'feature_categories.yml')).to_set + feature_categories = Gitlab::FeatureCategories.default.categories described_class::ROUTES.each do |route| expect(feature_categories).to include(route.feature_category), "#{route.name} has a category of #{route.feature_category}, which is not valid" diff --git a/spec/lib/gitlab/etag_caching/router/restful_spec.rb b/spec/lib/gitlab/etag_caching/router/restful_spec.rb index 1f5cac09b6d..a0fc480369c 100644 --- a/spec/lib/gitlab/etag_caching/router/restful_spec.rb +++ b/spec/lib/gitlab/etag_caching/router/restful_spec.rb @@ -107,7 +107,7 @@ RSpec.describe Gitlab::EtagCaching::Router::Restful do end it 'has a valid feature category for every route', :aggregate_failures do - feature_categories = YAML.load_file(Rails.root.join('config', 'feature_categories.yml')).to_set + feature_categories = Gitlab::FeatureCategories.default.categories described_class::ROUTES.each do |route| expect(feature_categories).to include(route.feature_category), "#{route.name} has a category of #{route.feature_category}, which is not valid" diff --git a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb index 192c0c7d02f..5870f9a8f68 100644 --- a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb @@ -349,7 +349,7 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do it 'has every label in config/feature_categories.yml' do defaults = [::Gitlab::FeatureCategories::FEATURE_CATEGORY_DEFAULT, 'not_owned'] - feature_categories = YAML.load_file(Rails.root.join('config', 'feature_categories.yml')).map(&:strip) + defaults + feature_categories = Gitlab::FeatureCategories.default.categories + defaults expect(described_class::FEATURE_CATEGORIES_TO_INITIALIZE).to all(be_in(feature_categories)) end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index a19acbd196d..087cd3287f8 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -2883,121 +2883,30 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end - describe '#execute_hooks' do + describe 'hooks trigerring' do let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) } - let!(:build_a) { create_build('a', 0) } - let!(:build_b) { create_build('b', 0) } + %i[ + enqueue + request_resource + prepare + run + skip + drop + succeed + cancel + block + delay + ].each do |action| + context "when pipeline action is #{action}" do + let(:pipeline_action) { action } - let!(:hook) do - create(:project_hook, pipeline_events: enabled) - end - - before do - WebHookWorker.drain - end - - context 'with pipeline hooks enabled' do - let(:enabled) { true } - - before do - stub_full_request(hook.url, method: :post) - end - - context 'with multiple builds', :sidekiq_inline do - context 'when build is queued' do - before do - build_a.reload.enqueue - build_b.reload.enqueue - end - - it 'receives a pending event once' do - expect(WebMock).to have_requested_pipeline_hook('pending').once - end - - it 'builds hook data once' do - create(:pipelines_email_integration) + it 'schedules a new PipelineHooksWorker job' do + expect(PipelineHooksWorker).to receive(:perform_async).with(pipeline.id) - expect(Gitlab::DataBuilder::Pipeline).to receive(:build).once.and_call_original - - pipeline.execute_hooks - end + pipeline.reload.public_send(pipeline_action) end - - context 'when build is run' do - before do - build_a.reload.enqueue - build_a.reload.run! - build_b.reload.enqueue - build_b.reload.run! - end - - it 'receives a running event once' do - expect(WebMock).to have_requested_pipeline_hook('running').once - end - end - - context 'when all builds succeed' do - before do - build_a.success - - # We have to reload build_b as this is in next stage and it gets triggered by PipelineProcessWorker - build_b.reload.success - end - - it 'receives a success event once' do - expect(WebMock).to have_requested_pipeline_hook('success').once - end - end - - context 'when stage one failed' do - let!(:build_b) { create_build('b', 1) } - - before do - build_a.drop - end - - it 'receives a failed event once' do - expect(WebMock).to have_requested_pipeline_hook('failed').once - end - end - - def have_requested_pipeline_hook(status) - have_requested(:post, stubbed_hostname(hook.url)).with do |req| - json_body = Gitlab::Json.parse(req.body) - json_body['object_attributes']['status'] == status && - json_body['builds'].length == 2 - end - end - end - end - - context 'with pipeline hooks disabled' do - let(:enabled) { false } - - before do - build_a.enqueue - build_b.enqueue end - - it 'did not execute pipeline_hook after touched' do - expect(WebMock).not_to have_requested(:post, hook.url) - end - - it 'does not build hook data' do - expect(Gitlab::DataBuilder::Pipeline).not_to receive(:build) - - pipeline.execute_hooks - end - end - - def create_build(name, stage_idx) - create(:ci_build, - :created, - pipeline: pipeline, - name: name, - stage: "stage:#{stage_idx}", - stage_idx: stage_idx) end end diff --git a/spec/services/ci/pipelines/hook_service_spec.rb b/spec/services/ci/pipelines/hook_service_spec.rb new file mode 100644 index 00000000000..0e1ef6afd0d --- /dev/null +++ b/spec/services/ci/pipelines/hook_service_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::Pipelines::HookService do + describe '#execute_hooks' do + let_it_be(:namespace) { create(:namespace) } + let_it_be(:project) { create(:project, :repository, namespace: namespace) } + let_it_be(:pipeline) { create(:ci_empty_pipeline, :created, project: project) } + + let(:hook_enabled) { true } + let!(:hook) { create(:project_hook, project: project, pipeline_events: hook_enabled) } + let(:hook_data) { double } + + subject(:service) { described_class.new(pipeline) } + + describe 'HOOK_NAME' do + specify { expect(described_class::HOOK_NAME).to eq(:pipeline_hooks) } + end + + context 'with pipeline hooks enabled' do + before do + allow(Gitlab::DataBuilder::Pipeline).to receive(:build).with(pipeline).once.and_return(hook_data) + end + + it 'calls pipeline.project.execute_hooks and pipeline.project.execute_integrations' do + create(:pipelines_email_integration, project: project) + + expect(pipeline.project).to receive(:execute_hooks).with(hook_data, described_class::HOOK_NAME) + expect(pipeline.project).to receive(:execute_integrations).with(hook_data, described_class::HOOK_NAME) + + service.execute + end + end + + context 'with pipeline hooks and integrations disabled' do + let(:hook_enabled) { false } + + it 'does not call pipeline.project.execute_hooks and pipeline.project.execute_integrations' do + expect(pipeline.project).not_to receive(:execute_hooks) + expect(pipeline.project).not_to receive(:execute_integrations) + + service.execute + end + end + end +end diff --git a/spec/services/ci/stuck_builds/drop_service_spec.rb b/spec/services/ci/stuck_builds/drop_pending_service_spec.rb index d57fbbe895a..aa0526edf57 100644 --- a/spec/services/ci/stuck_builds/drop_service_spec.rb +++ b/spec/services/ci/stuck_builds/drop_pending_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::StuckBuilds::DropService do +RSpec.describe Ci::StuckBuilds::DropPendingService do let!(:runner) { create :ci_runner } let!(:job) { create :ci_build, runner: runner } let(:created_at) { } diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb index bb4767c9c11..9a4b27997e9 100644 --- a/spec/workers/every_sidekiq_worker_spec.rb +++ b/spec/workers/every_sidekiq_worker_spec.rb @@ -48,7 +48,7 @@ RSpec.describe 'Every Sidekiq worker' do describe "feature category declarations" do let(:feature_categories) do - YAML.load_file(Rails.root.join('config', 'feature_categories.yml')).map(&:to_sym).to_set + Gitlab::FeatureCategories.default.categories.map(&:to_sym).to_set end # All Sidekiq worker classes should declare a valid `feature_category` @@ -161,6 +161,7 @@ RSpec.describe 'Every Sidekiq worker' do 'Ci::DropPipelineWorker' => 3, 'Ci::InitialPipelineProcessWorker' => 3, 'Ci::MergeRequests::AddTodoWhenBuildFailsWorker' => 3, + 'Ci::Minutes::UpdateProjectAndNamespaceUsageWorker' => 3, 'Ci::PipelineArtifacts::CoverageReportWorker' => 3, 'Ci::PipelineArtifacts::CreateQualityReportWorker' => 3, 'Ci::PipelineBridgeStatusWorker' => 3, diff --git a/spec/workers/pipeline_hooks_worker_spec.rb b/spec/workers/pipeline_hooks_worker_spec.rb index 0ed00c0c66a..13a86c3d4fe 100644 --- a/spec/workers/pipeline_hooks_worker_spec.rb +++ b/spec/workers/pipeline_hooks_worker_spec.rb @@ -8,8 +8,10 @@ RSpec.describe PipelineHooksWorker do let(:pipeline) { create(:ci_pipeline) } it 'executes hooks for the pipeline' do - expect_any_instance_of(Ci::Pipeline) - .to receive(:execute_hooks) + hook_service = double + + expect(Ci::Pipelines::HookService).to receive(:new).and_return(hook_service) + expect(hook_service).to receive(:execute) described_class.new.perform(pipeline.id) end @@ -17,6 +19,8 @@ RSpec.describe PipelineHooksWorker do context 'when pipeline does not exist' do it 'does not raise exception' do + expect(Ci::Pipelines::HookService).not_to receive(:new) + expect { described_class.new.perform(123) } .not_to raise_error end diff --git a/spec/workers/stuck_ci_jobs_worker_spec.rb b/spec/workers/stuck_ci_jobs_worker_spec.rb index aedf7f1c0b5..19ff8ec55c2 100644 --- a/spec/workers/stuck_ci_jobs_worker_spec.rb +++ b/spec/workers/stuck_ci_jobs_worker_spec.rb @@ -23,10 +23,10 @@ RSpec.describe StuckCiJobsWorker do subject end - it 'executes an instance of Ci::StuckBuilds::DropService' do + it 'executes an instance of Ci::StuckBuilds::DropPendingService' do expect_to_obtain_exclusive_lease(worker.lease_key, lease_uuid) - expect_next_instance_of(Ci::StuckBuilds::DropService) do |service| + expect_next_instance_of(Ci::StuckBuilds::DropPendingService) do |service| expect(service).to receive(:execute).exactly(:once) end |