Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-10-08 12:11:10 +00:00
parent d4c5231ca2
commit af97e4dd4b
76 changed files with 1031 additions and 1161 deletions

View File

@ -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

View File

@ -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'

View File

@ -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)

View File

@ -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.') },

View File

@ -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>

View File

@ -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');

View File

@ -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',
);

View File

@ -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;

View File

@ -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"

View File

@ -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>

View File

@ -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"

View File

@ -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() {

View File

@ -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"
>

View File

@ -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'],

View File

@ -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"
/>

View File

@ -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?

View File

@ -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

View File

@ -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),

View File

@ -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:)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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)

View File

@ -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/"
```

View File

@ -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.

View File

@ -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"`.

View File

@ -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 |

View File

@ -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**

View File

@ -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

View File

@ -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

View File

@ -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] ""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 = {

View File

@ -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);
});

View File

@ -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;

View File

@ -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) => {

View File

@ -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 () => {

View File

@ -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];

View File

@ -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, {

View File

@ -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,

View File

@ -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;

View File

@ -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);

View File

@ -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', () => {

View File

@ -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,

View File

@ -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);
};

View File

@ -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`;

View File

@ -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,

View File

@ -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: {},

View File

@ -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);

View File

@ -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;

View File

@ -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');

View File

@ -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');

View File

@ -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([[]]);
});
});
});

View File

@ -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);
});

View File

@ -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');
});
});

View File

@ -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);
});

View File

@ -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);

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) { }

View File

@ -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,

View File

@ -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

View File

@ -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