Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
d4c5231ca2
commit
af97e4dd4b
|
|
@ -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
|
||||
|
|
|
|||
2
Gemfile
2
Gemfile
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.') },
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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),
|
||||
|
|
@ -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:)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
5
bin/rake
5
bin/rake
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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/"
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"`.
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
@ -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**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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] ""
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ module QA
|
|||
element :registry_image_content
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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: {},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
findDeleteButton().vm.$emit('delete');
|
||||
expect(findDeleteButton().exists()).toBe(true);
|
||||
expect(findDeleteButton().attributes()).toMatchObject({
|
||||
variant: 'danger',
|
||||
});
|
||||
expect(findDeleteButton().text()).toBe(REMOVE_TAG_BUTTON_TITLE);
|
||||
});
|
||||
|
||||
expect(wrapper.emitted('delete')).toEqual([[]]);
|
||||
it('delete event emits delete', () => {
|
||||
mountComponent();
|
||||
|
||||
findDeleteButton().vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted('delete')).toEqual([[]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2883,122 +2883,31 @@ 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
|
||||
it 'schedules a new PipelineHooksWorker job' do
|
||||
expect(PipelineHooksWorker).to receive(:perform_async).with(pipeline.id)
|
||||
|
||||
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)
|
||||
|
||||
expect(Gitlab::DataBuilder::Pipeline).to receive(:build).once.and_call_original
|
||||
|
||||
pipeline.execute_hooks
|
||||
end
|
||||
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
|
||||
pipeline.reload.public_send(pipeline_action)
|
||||
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
|
||||
|
||||
describe "#merge_requests_as_head_pipeline" do
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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) { }
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue