Commit 9c86e4b5 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 870ea48a 924ec18e
export const SNOWPLOW_JS_SOURCE = 'gitlab-javascript';
import { SNOWPLOW_JS_SOURCE } from './constants';
export default function getStandardContext({ extra = {} } = {}) {
const { schema, data = {} } = { ...window.gl?.snowplowStandardContext };
return {
schema,
data: {
...data,
source: SNOWPLOW_JS_SOURCE,
extra: extra || data.extra,
},
};
}
import { omitBy, isUndefined } from 'lodash'; import { omitBy, isUndefined } from 'lodash';
import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants'; import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
import { getExperimentData } from '~/experimentation/utils'; import { getExperimentData } from '~/experimentation/utils';
import getStandardContext from './get_standard_context';
const standardContext = { ...window.gl?.snowplowStandardContext };
export const STANDARD_CONTEXT = {
schema: standardContext.schema,
data: {
...(standardContext.data || {}),
source: 'gitlab-javascript',
},
};
const DEFAULT_SNOWPLOW_OPTIONS = { const DEFAULT_SNOWPLOW_OPTIONS = {
namespace: 'gl', namespace: 'gl',
...@@ -44,19 +35,41 @@ const addExperimentContext = (opts) => { ...@@ -44,19 +35,41 @@ const addExperimentContext = (opts) => {
}; };
const createEventPayload = (el, { suffix = '' } = {}) => { const createEventPayload = (el, { suffix = '' } = {}) => {
const action = (el.dataset.trackAction || el.dataset.trackEvent) + (suffix || ''); const {
let value = el.dataset.trackValue || el.value || undefined; trackAction,
trackEvent,
trackValue,
trackExtra,
trackExperiment,
trackContext,
trackLabel,
trackProperty,
} = el?.dataset || {};
const action = (trackAction || trackEvent) + (suffix || '');
let value = trackValue || el.value || undefined;
if (el.type === 'checkbox' && !el.checked) value = false; if (el.type === 'checkbox' && !el.checked) value = false;
let extra = trackExtra;
if (extra !== undefined) {
try {
extra = JSON.parse(extra);
} catch (e) {
extra = undefined;
}
}
const context = addExperimentContext({ const context = addExperimentContext({
experiment: el.dataset.trackExperiment, experiment: trackExperiment,
context: el.dataset.trackContext, context: trackContext,
}); });
const data = { const data = {
label: el.dataset.trackLabel, label: trackLabel,
property: el.dataset.trackProperty, property: trackProperty,
value, value,
extra,
...context, ...context,
}; };
...@@ -88,8 +101,10 @@ const dispatchEvent = (category = document.body.dataset.page, action = 'generic' ...@@ -88,8 +101,10 @@ const dispatchEvent = (category = document.body.dataset.page, action = 'generic'
// eslint-disable-next-line @gitlab/require-i18n-strings // eslint-disable-next-line @gitlab/require-i18n-strings
if (!category) throw new Error('Tracking: no category provided for tracking.'); if (!category) throw new Error('Tracking: no category provided for tracking.');
const { label, property, value } = data; const { label, property, value, extra = {} } = data;
const contexts = [STANDARD_CONTEXT];
const standardContext = getStandardContext({ extra });
const contexts = [standardContext];
if (data.context) { if (data.context) {
contexts.push(data.context); contexts.push(data.context);
...@@ -165,13 +180,18 @@ export default class Tracking { ...@@ -165,13 +180,18 @@ export default class Tracking {
throw new Error('Unable to enable form event tracking without allow rules.'); throw new Error('Unable to enable form event tracking without allow rules.');
} }
contexts.unshift(STANDARD_CONTEXT); // Ignore default/standard schema
const standardContext = getStandardContext();
const userProvidedContexts = contexts.filter(
(context) => context.schema !== standardContext.schema,
);
const mappedConfig = { const mappedConfig = {
forms: { whitelist: config.forms?.allow || [] }, forms: { whitelist: config.forms?.allow || [] },
fields: { whitelist: config.fields?.allow || [] }, fields: { whitelist: config.fields?.allow || [] },
}; };
const enabler = () => window.snowplow('enableFormTracking', mappedConfig, contexts); const enabler = () => window.snowplow('enableFormTracking', mappedConfig, userProvidedContexts);
if (document.readyState !== 'loading') enabler(); if (document.readyState !== 'loading') enabler();
else document.addEventListener('DOMContentLoaded', enabler); else document.addEventListener('DOMContentLoaded', enabler);
...@@ -220,7 +240,8 @@ export function initDefaultTrackers() { ...@@ -220,7 +240,8 @@ export function initDefaultTrackers() {
window.snowplow('enableActivityTracking', 30, 30); window.snowplow('enableActivityTracking', 30, 30);
// must be after enableActivityTracking // must be after enableActivityTracking
window.snowplow('trackPageView', null, [STANDARD_CONTEXT]); const standardContext = getStandardContext();
window.snowplow('trackPageView', null, [standardContext]);
if (window.snowplowOptions.formTracking) Tracking.enableFormTracking(opts.formTrackingConfig); if (window.snowplowOptions.formTracking) Tracking.enableFormTracking(opts.formTrackingConfig);
if (window.snowplowOptions.linkClickTracking) window.snowplow('enableLinkClickTracking'); if (window.snowplowOptions.linkClickTracking) window.snowplow('enableLinkClickTracking');
......
...@@ -155,13 +155,13 @@ Snowplow JS adds many [web-specific parameters](https://docs.snowplowanalytics.c ...@@ -155,13 +155,13 @@ Snowplow JS adds many [web-specific parameters](https://docs.snowplowanalytics.c
## Implementing Snowplow JS (Frontend) tracking ## Implementing Snowplow JS (Frontend) tracking
GitLab provides `Tracking`, an interface that wraps the [Snowplow JavaScript Tracker](https://github.com/snowplow/snowplow/wiki/javascript-tracker) for tracking custom events. The simplest way to use it is to add `data-` attributes to clickable elements and dropdowns. There is also a Vue mixin (exposing a `track` method), and the static method `Tracking.event`. Each of these requires at minimum a `category` and an `action`. Additional data can be provided that adheres to our [Structured event taxonomy](#structured-event-taxonomy). GitLab provides `Tracking`, an interface that wraps the [Snowplow JavaScript Tracker](https://github.com/snowplow/snowplow/wiki/javascript-tracker) for tracking custom events. The simplest way to use it is to add `data-` attributes to clickable elements and dropdowns. There is also a Vue mixin (exposing a `track` method), and the static method `Tracking.event`. Each of these requires at minimum a `category` and an `action`. You can provide additional [Structured event taxonomy](#structured-event-taxonomy) properties along with an `extra` object that accepts key-value pairs.
| field | type | default value | description | | field | type | default value | description |
|:-----------|:-------|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |:-----------|:-------|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `category` | string | `document.body.dataset.page` | Page or subsection of a page that events are being captured within. | | `category` | string | `document.body.dataset.page` | Page or subsection of a page that events are being captured within. |
| `action` | string | generic | Action the user is taking. Clicks should be `click` and activations should be `activate`, so for example, focusing a form field would be `activate_form_input`, and clicking a button would be `click_button`. | | `action` | string | generic | Action the user is taking. Clicks should be `click` and activations should be `activate`, so for example, focusing a form field would be `activate_form_input`, and clicking a button would be `click_button`. |
| `data` | object | `{}` | Additional data such as `label`, `property`, `value`, and `context` as described in our [Structured event taxonomy](#structured-event-taxonomy). | | `data` | object | `{}` | Additional data such as `label`, `property`, `value`, `context` (as described in our [Structured event taxonomy](#structured-event-taxonomy)), and `extra` (key-value pairs object). |
### Usage recommendations ### Usage recommendations
...@@ -171,7 +171,7 @@ GitLab provides `Tracking`, an interface that wraps the [Snowplow JavaScript Tra ...@@ -171,7 +171,7 @@ GitLab provides `Tracking`, an interface that wraps the [Snowplow JavaScript Tra
### Tracking with data attributes ### Tracking with data attributes
When working within HAML (or Vue templates) we can add `data-track-*` attributes to elements of interest. All elements that have a `data-track-action` attribute automatically have event tracking bound on clicks. When working within HAML (or Vue templates) we can add `data-track-*` attributes to elements of interest. All elements that have a `data-track-action` attribute automatically have event tracking bound on clicks. You can provide extra data as a valid JSON string using `data-track-extra`.
Below is an example of `data-track-*` attributes assigned to a button: Below is an example of `data-track-*` attributes assigned to a button:
...@@ -184,6 +184,7 @@ Below is an example of `data-track-*` attributes assigned to a button: ...@@ -184,6 +184,7 @@ Below is an example of `data-track-*` attributes assigned to a button:
data-track-action="click_button" data-track-action="click_button"
data-track-label="template_preview" data-track-label="template_preview"
data-track-property="my-template" data-track-property="my-template"
data-track-extra='{ "template_variant": "primary" }'
/> />
``` ```
...@@ -197,6 +198,7 @@ Below is a list of supported `data-track-*` attributes: ...@@ -197,6 +198,7 @@ Below is a list of supported `data-track-*` attributes:
| `data-track-label` | false | The `label` as described in our [Structured event taxonomy](#structured-event-taxonomy). | | `data-track-label` | false | The `label` as described in our [Structured event taxonomy](#structured-event-taxonomy). |
| `data-track-property` | false | The `property` as described in our [Structured event taxonomy](#structured-event-taxonomy). | | `data-track-property` | false | The `property` as described in our [Structured event taxonomy](#structured-event-taxonomy). |
| `data-track-value` | false | The `value` as described in our [Structured event taxonomy](#structured-event-taxonomy). If omitted, this is the element's `value` property or an empty string. For checkboxes, the default value is the element's checked attribute or `false` when unchecked. | | `data-track-value` | false | The `value` as described in our [Structured event taxonomy](#structured-event-taxonomy). If omitted, this is the element's `value` property or an empty string. For checkboxes, the default value is the element's checked attribute or `false` 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). | | `data-track-context` | false | The `context` as described in our [Structured event taxonomy](#structured-event-taxonomy). |
#### Available helpers #### Available helpers
...@@ -287,6 +289,7 @@ export default { ...@@ -287,6 +289,7 @@ export default {
// category: '', // category: '',
// property: '', // property: '',
// value: '', // value: '',
// extra: {},
}, },
}; };
}, },
...@@ -357,6 +360,10 @@ button.addEventListener('click', () => { ...@@ -357,6 +360,10 @@ button.addEventListener('click', () => {
Tracking.event('dashboard:projects:index', 'click_button', { Tracking.event('dashboard:projects:index', 'click_button', {
label: 'create_from_template', label: 'create_from_template',
property: 'template_preview', property: 'template_preview',
extra: {
templateVariant: 'primary',
valid: true,
},
}); });
}); });
``` ```
...@@ -381,6 +388,10 @@ describe('MyTracking', () => { ...@@ -381,6 +388,10 @@ describe('MyTracking', () => {
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', { expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'create_from_template', label: 'create_from_template',
property: 'template_preview', property: 'template_preview',
extra: {
templateVariant: 'primary',
valid: true,
},
}); });
}); });
}); });
...@@ -446,7 +457,7 @@ There are several tools for developing and testing Snowplow Event ...@@ -446,7 +457,7 @@ There are several tools for developing and testing Snowplow Event
### Test frontend events ### Test frontend events
To test frontend events in development: To test frontend events in development:
- [Enable Snowplow in the admin area](#enabling-snowplow). - [Enable Snowplow in the admin area](#enabling-snowplow).
- Turn off any ad blockers that would prevent Snowplow JS from loading in your environment. - Turn off any ad blockers that would prevent Snowplow JS from loading in your environment.
......
...@@ -65,53 +65,65 @@ to draw the visualization on the merge request expires **one week** after creati ...@@ -65,53 +65,65 @@ to draw the visualization on the merge request expires **one week** after creati
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217664) in GitLab 13.8. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217664) in GitLab 13.8.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/284822) in GitLab 13.9. > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/284822) in GitLab 13.9.
For the coverage report to properly match the files displayed on a merge request diff, the `filename` of a `class` element The coverage report properly matches changed files only if the `filename` of a `class` element
must contain the full path relative to the project root. But in some coverage analysis frameworks, the generated contains the full path relative to the project root. However, in some coverage analysis frameworks,
Cobertura XML has the `filename` path relative to the class package directory instead. the generated Cobertura XML has the `filename` path relative to the class package directory instead.
To make an intelligent guess on the project root relative `class` path, the Cobertura XML parser attempts to build the To make an intelligent guess on the project root relative `class` path, the Cobertura XML parser
full path by doing the following: attempts to build the full path by:
1. Extract a portion of the `source` paths from the `sources` element and combine them with the class `filename` path. - Extracting a portion of the `source` paths from the `sources` element and combining them with the
1. Check if the candidate path exists in the project. class `filename` path.
1. Use the first candidate that matches as the class full path. - Checking if the candidate path exists in the project.
- Using the first candidate that matches as the class full path.
As an example scenario, given the project's full path is `test-org/test-project`, and has the following file tree relative #### Path correction example
to the project root:
```shell As an example, a project with:
Auth/User.cs
Lib/Utils/User.cs
src/main/java
```
In the Cobertura XML, the `filename` attribute in the `class` element assumes the value is a - A full path of `test-org/test-project`.
relative path to project's root. - The following files relative to the project root:
```xml ```shell
<class name="packet.name" filename="src/main/java" line-rate="0.0" branch-rate="0.0" complexity="5"> Auth/User.cs
``` Lib/Utils/User.cs
src/main/java
```
And the `sources` from Cobertura XML with paths in the format of `<CI_BUILDS_DIR>/<PROJECT_FULL_PATH>/...`: In the:
```xml - Cobertura XML, the `filename` attribute in the `class` element assumes the value is a relative
<sources> path to the project's root:
<source>/builds/test-org/test-project/Auth</source>
<source>/builds/test-org/test-project/Lib/Utils</source> ```xml
</sources> <class name="packet.name" filename="src/main/java" line-rate="0.0" branch-rate="0.0" complexity="5">
``` ```
- `sources` from Cobertura XML, the following paths in the format
`<CI_BUILDS_DIR>/<PROJECT_FULL_PATH>/...`:
The parser extracts `Auth` and `Lib/Utils` from the sources and use these as basis to determine the class path relative to ```xml
the project root, combining these extracted sources and the class filename. <sources>
<source>/builds/test-org/test-project/Auth</source>
<source>/builds/test-org/test-project/Lib/Utils</source>
</sources>
```
If for example there is a `class` element with the `filename` value of `User.cs`, the parser takes the first candidate path The parser:
that matches, which is `Auth/User.cs`.
For each `class` element, the parser attempts to look for a match for each extracted `source` path up to `100` iterations. If it reaches this limit without finding a matching path in the file tree, the class will not be included in the final coverage report. - Extracts `Auth` and `Lib/Utils` from the `sources` and uses these to determine the `class` path
relative to the project root.
- Combines these extracted `sources` and the class filename. For example, if there is a `class`
element with the `filename` value of `User.cs`, the parser takes the first candidate path that
matches, which is `Auth/User.cs`.
- For each `class` element, attempts to look for a match for each extracted `source` path up to
100 iterations. If it reaches this limit without finding a matching path in the file tree, the
class is not included in the final coverage report.
NOTE: NOTE:
The automatic class path correction only works on `source` paths in the format of `<CI_BUILDS_DIR>/<PROJECT_FULL_PATH>/...`. If `source` will be ignored if the path does not follow this pattern. The parser assumes that Automatic class path correction only works on `source` paths in the format `<CI_BUILDS_DIR>/<PROJECT_FULL_PATH>/...`.
the `filename` of a `class` element contains the full path relative to the project root. The `source` is ignored if the path does not follow this pattern. The parser assumes that the
`filename` of a `class` element contains the full path relative to the project root.
## Example test coverage configurations ## Example test coverage configurations
......
...@@ -6,17 +6,18 @@ ...@@ -6,17 +6,18 @@
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' } %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand') = expanded ? _('Collapse') : _('Expand')
%p %p
= _('Set instance-wide template repository') = _('Select a shared template repository for all projects on this instance.')
.settings-content .settings-content
= form_for @application_setting, url: templates_admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_for @application_setting, url: templates_admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting) = form_errors(@application_setting)
%fieldset %fieldset
.form-group .form-group
= f.label :file_template_project_id, class: 'label-light' do = f.label :file_template_project_id, class: 'label-bold' do
.form-text.text-muted .form-text
Select a = _('Select a template repository')
= link_to 'template repository', help_page_path("user/admin_area/settings/instance_template_repository")
= project_select_tag('application_setting[file_template_project_id]', class: 'project-item-select hidden-filter-value', toggle_class: 'js-project-search js-project-filter js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit', = project_select_tag('application_setting[file_template_project_id]', class: 'project-item-select hidden-filter-value', toggle_class: 'js-project-search js-project-filter js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
placeholder: _('Search projects'), idAttribute: 'id', data: { order_by: 'last_activity_at', idattribute: 'id', all_projects: 'true', simple_filter: true, allow_clear: true }, value: @application_setting.file_template_project_id) placeholder: _('Search projects'), idAttribute: 'id', data: { order_by: 'last_activity_at', idattribute: 'id', all_projects: 'true', simple_filter: true, allow_clear: true }, value: @application_setting.file_template_project_id)
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('user/admin_area/settings/instance_template_repository') }
= s_('TemplateRepository|Select a repository to make its templates available to all projects. %{link_start}What should the repository contain?%{link_end} ').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
= f.submit 'Save changes', class: "gl-button btn btn-confirm" = f.submit 'Save changes', class: "gl-button btn btn-confirm"
...@@ -29360,6 +29360,9 @@ msgstr "" ...@@ -29360,6 +29360,9 @@ msgstr ""
msgid "Select a repository" msgid "Select a repository"
msgstr "" msgstr ""
msgid "Select a shared template repository for all projects on this instance."
msgstr ""
msgid "Select a template repository" msgid "Select a template repository"
msgstr "" msgstr ""
...@@ -29750,9 +29753,6 @@ msgstr "" ...@@ -29750,9 +29753,6 @@ msgstr ""
msgid "Set due date" msgid "Set due date"
msgstr "" msgstr ""
msgid "Set instance-wide template repository"
msgstr ""
msgid "Set iteration" msgid "Set iteration"
msgstr "" msgstr ""
...@@ -32001,6 +32001,9 @@ msgstr "" ...@@ -32001,6 +32001,9 @@ msgstr ""
msgid "Template to append to all Service Desk issues" msgid "Template to append to all Service Desk issues"
msgstr "" msgstr ""
msgid "TemplateRepository|Select a repository to make its templates available to all projects. %{link_start}What should the repository contain?%{link_end} "
msgstr ""
msgid "Templates" msgid "Templates"
msgstr "" msgstr ""
......
import { SNOWPLOW_JS_SOURCE } from '~/tracking/constants';
import getStandardContext from '~/tracking/get_standard_context';
describe('~/tracking/get_standard_context', () => {
beforeEach(() => {
window.gl = window.gl || {};
window.gl.snowplowStandardContext = {};
});
it('returns default object if called without server context', () => {
expect(getStandardContext()).toStrictEqual({
schema: undefined,
data: {
source: SNOWPLOW_JS_SOURCE,
extra: {},
},
});
});
it('returns filled object if called with server context', () => {
window.gl.snowplowStandardContext = {
schema: 'iglu:com.gitlab/gitlab_standard',
data: {
environment: 'testing',
},
};
expect(getStandardContext()).toStrictEqual({
schema: 'iglu:com.gitlab/gitlab_standard',
data: {
environment: 'testing',
source: SNOWPLOW_JS_SOURCE,
extra: {},
},
});
});
it('always overrides `source` property', () => {
window.gl.snowplowStandardContext = {
data: {
source: 'custom_source',
},
};
expect(getStandardContext().data.source).toBe(SNOWPLOW_JS_SOURCE);
});
it('accepts optional `extra` property', () => {
const extra = { foo: 'bar' };
expect(getStandardContext({ extra }).data.extra).toBe(extra);
});
});
import { setHTMLFixture } from 'helpers/fixtures'; import { setHTMLFixture } from 'helpers/fixtures';
import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants'; import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
import { getExperimentData } from '~/experimentation/utils'; import { getExperimentData } from '~/experimentation/utils';
import Tracking, { initUserTracking, initDefaultTrackers, STANDARD_CONTEXT } from '~/tracking'; import Tracking, { initUserTracking, initDefaultTrackers } from '~/tracking';
import getStandardContext from '~/tracking/get_standard_context';
jest.mock('~/experimentation/utils', () => ({ getExperimentData: jest.fn() })); jest.mock('~/experimentation/utils', () => ({ getExperimentData: jest.fn() }));
describe('Tracking', () => { describe('Tracking', () => {
let standardContext;
let snowplowSpy; let snowplowSpy;
let bindDocumentSpy; let bindDocumentSpy;
let trackLoadEventsSpy; let trackLoadEventsSpy;
let enableFormTracking; let enableFormTracking;
beforeAll(() => {
window.gl = window.gl || {};
window.gl.snowplowStandardContext = {
schema: 'iglu:com.gitlab/gitlab_standard',
data: {
environment: 'testing',
source: 'unknown',
extra: {},
},
};
standardContext = getStandardContext();
});
beforeEach(() => { beforeEach(() => {
getExperimentData.mockReturnValue(undefined); getExperimentData.mockReturnValue(undefined);
...@@ -59,7 +75,7 @@ describe('Tracking', () => { ...@@ -59,7 +75,7 @@ describe('Tracking', () => {
it('should activate features based on what has been enabled', () => { it('should activate features based on what has been enabled', () => {
initDefaultTrackers(); initDefaultTrackers();
expect(snowplowSpy).toHaveBeenCalledWith('enableActivityTracking', 30, 30); expect(snowplowSpy).toHaveBeenCalledWith('enableActivityTracking', 30, 30);
expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', null, [STANDARD_CONTEXT]); expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', null, [standardContext]);
expect(snowplowSpy).not.toHaveBeenCalledWith('enableFormTracking'); expect(snowplowSpy).not.toHaveBeenCalledWith('enableFormTracking');
expect(snowplowSpy).not.toHaveBeenCalledWith('enableLinkClickTracking'); expect(snowplowSpy).not.toHaveBeenCalledWith('enableLinkClickTracking');
...@@ -93,34 +109,6 @@ describe('Tracking', () => { ...@@ -93,34 +109,6 @@ describe('Tracking', () => {
navigator.msDoNotTrack = undefined; navigator.msDoNotTrack = undefined;
}); });
describe('builds the standard context', () => {
let standardContext;
beforeAll(async () => {
window.gl = window.gl || {};
window.gl.snowplowStandardContext = {
schema: 'iglu:com.gitlab/gitlab_standard',
data: {
environment: 'testing',
source: 'unknown',
},
};
jest.resetModules();
({ STANDARD_CONTEXT: standardContext } = await import('~/tracking'));
});
it('uses server data', () => {
expect(standardContext.schema).toBe('iglu:com.gitlab/gitlab_standard');
expect(standardContext.data.environment).toBe('testing');
});
it('overrides schema source', () => {
expect(standardContext.data.source).toBe('gitlab-javascript');
});
});
it('tracks to snowplow (our current tracking system)', () => { it('tracks to snowplow (our current tracking system)', () => {
Tracking.event('_category_', '_eventName_', { label: '_label_' }); Tracking.event('_category_', '_eventName_', { label: '_label_' });
...@@ -131,7 +119,31 @@ describe('Tracking', () => { ...@@ -131,7 +119,31 @@ describe('Tracking', () => {
'_label_', '_label_',
undefined, undefined,
undefined, undefined,
[STANDARD_CONTEXT], [standardContext],
);
});
it('allows adding extra data to the default context', () => {
const extra = { foo: 'bar' };
Tracking.event('_category_', '_eventName_', { extra });
expect(snowplowSpy).toHaveBeenCalledWith(
'trackStructEvent',
'_category_',
'_eventName_',
undefined,
undefined,
undefined,
[
{
...standardContext,
data: {
...standardContext.data,
extra,
},
},
],
); );
}); });
...@@ -165,14 +177,14 @@ describe('Tracking', () => { ...@@ -165,14 +177,14 @@ describe('Tracking', () => {
}); });
describe('.enableFormTracking', () => { describe('.enableFormTracking', () => {
it('tells snowplow to enable form tracking', () => { it('tells snowplow to enable form tracking, with only explicit contexts', () => {
const config = { forms: { allow: ['form-class1'] }, fields: { allow: ['input-class1'] } }; const config = { forms: { allow: ['form-class1'] }, fields: { allow: ['input-class1'] } };
Tracking.enableFormTracking(config, ['_passed_context_']); Tracking.enableFormTracking(config, ['_passed_context_', standardContext]);
expect(snowplowSpy).toHaveBeenCalledWith( expect(snowplowSpy).toHaveBeenCalledWith(
'enableFormTracking', 'enableFormTracking',
{ forms: { whitelist: ['form-class1'] }, fields: { whitelist: ['input-class1'] } }, { forms: { whitelist: ['form-class1'] }, fields: { whitelist: ['input-class1'] } },
[{ data: { source: 'gitlab-javascript' }, schema: undefined }, '_passed_context_'], ['_passed_context_'],
); );
}); });
...@@ -203,7 +215,7 @@ describe('Tracking', () => { ...@@ -203,7 +215,7 @@ describe('Tracking', () => {
'_label_', '_label_',
undefined, undefined,
undefined, undefined,
[STANDARD_CONTEXT], [standardContext],
); );
}); });
}); });
...@@ -226,6 +238,8 @@ describe('Tracking', () => { ...@@ -226,6 +238,8 @@ describe('Tracking', () => {
<div data-track-${term}="nested_event"><span class="nested"></span></div> <div data-track-${term}="nested_event"><span class="nested"></span></div>
<input data-track-bogus="click_bogusinput" data-track-label="_label_" value="_value_"/> <input data-track-bogus="click_bogusinput" data-track-label="_label_" value="_value_"/>
<input data-track-${term}="click_input3" data-track-experiment="example" value="_value_"/> <input data-track-${term}="click_input3" data-track-experiment="example" value="_value_"/>
<input data-track-${term}="event_with_extra" data-track-extra='{ "foo": "bar" }' />
<input data-track-${term}="event_with_invalid_extra" data-track-extra="invalid_json" />
`); `);
}); });
...@@ -301,6 +315,20 @@ describe('Tracking', () => { ...@@ -301,6 +315,20 @@ describe('Tracking', () => {
context: { schema: TRACKING_CONTEXT_SCHEMA, data: mockExperimentData }, context: { schema: TRACKING_CONTEXT_SCHEMA, data: mockExperimentData },
}); });
}); });
it('supports extra data as JSON', () => {
document.querySelector(`[data-track-${term}="event_with_extra"]`).click();
expect(eventSpy).toHaveBeenCalledWith('_category_', 'event_with_extra', {
extra: { foo: 'bar' },
});
});
it('ignores extra if provided JSON is invalid', () => {
document.querySelector(`[data-track-${term}="event_with_invalid_extra"]`).click();
expect(eventSpy).toHaveBeenCalledWith('_category_', 'event_with_invalid_extra', {});
});
}); });
describe.each` describe.each`
......
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment