2020-05-24 23:13:21 +05:30
|
|
|
import { omitBy, isUndefined } from 'lodash';
|
2021-04-17 20:07:23 +05:30
|
|
|
import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
|
|
|
|
import { getExperimentData } from '~/experimentation/utils';
|
2021-09-04 01:27:46 +05:30
|
|
|
import getStandardContext from './get_standard_context';
|
2019-10-12 21:52:04 +05:30
|
|
|
|
2019-12-04 20:38:33 +05:30
|
|
|
const DEFAULT_SNOWPLOW_OPTIONS = {
|
|
|
|
namespace: 'gl',
|
|
|
|
hostname: window.location.hostname,
|
|
|
|
cookieDomain: window.location.hostname,
|
|
|
|
appId: '',
|
|
|
|
userFingerprint: false,
|
|
|
|
respectDoNotTrack: true,
|
|
|
|
forceSecureTracker: true,
|
|
|
|
eventMethod: 'post',
|
2021-01-03 14:25:43 +05:30
|
|
|
contexts: { webPage: true, performanceTiming: true },
|
2019-12-04 20:38:33 +05:30
|
|
|
formTracking: false,
|
|
|
|
linkClickTracking: false,
|
2021-01-29 00:20:46 +05:30
|
|
|
pageUnloadTimer: 10,
|
2021-09-04 01:27:46 +05:30
|
|
|
formTrackingConfig: {
|
|
|
|
forms: { allow: [] },
|
|
|
|
fields: { allow: [] },
|
|
|
|
},
|
2019-12-04 20:38:33 +05:30
|
|
|
};
|
|
|
|
|
2021-04-29 21:17:54 +05:30
|
|
|
const addExperimentContext = (opts) => {
|
|
|
|
const { experiment, ...options } = opts;
|
|
|
|
if (experiment) {
|
|
|
|
const data = getExperimentData(experiment);
|
|
|
|
if (data) {
|
|
|
|
const context = { schema: TRACKING_CONTEXT_SCHEMA, data };
|
|
|
|
return { ...options, context };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return options;
|
|
|
|
};
|
|
|
|
|
2021-09-30 23:02:18 +05:30
|
|
|
const renameKey = (o, oldKey, newKey) => {
|
|
|
|
const ret = {};
|
|
|
|
delete Object.assign(ret, o, { [newKey]: o[oldKey] })[oldKey];
|
|
|
|
return ret;
|
|
|
|
};
|
|
|
|
|
2020-05-24 23:13:21 +05:30
|
|
|
const createEventPayload = (el, { suffix = '' } = {}) => {
|
2021-09-04 01:27:46 +05:30
|
|
|
const {
|
|
|
|
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 = 0;
|
|
|
|
|
|
|
|
let extra = trackExtra;
|
|
|
|
|
|
|
|
if (extra !== undefined) {
|
|
|
|
try {
|
|
|
|
extra = JSON.parse(extra);
|
|
|
|
} catch (e) {
|
|
|
|
extra = undefined;
|
|
|
|
}
|
|
|
|
}
|
2019-12-21 20:55:43 +05:30
|
|
|
|
2021-04-29 21:17:54 +05:30
|
|
|
const context = addExperimentContext({
|
2021-09-04 01:27:46 +05:30
|
|
|
experiment: trackExperiment,
|
|
|
|
context: trackContext,
|
2021-04-29 21:17:54 +05:30
|
|
|
});
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2019-12-21 20:55:43 +05:30
|
|
|
const data = {
|
2021-09-04 01:27:46 +05:30
|
|
|
label: trackLabel,
|
|
|
|
property: trackProperty,
|
2019-12-21 20:55:43 +05:30
|
|
|
value,
|
2021-09-04 01:27:46 +05:30
|
|
|
extra,
|
2021-04-29 21:17:54 +05:30
|
|
|
...context,
|
2019-12-21 20:55:43 +05:30
|
|
|
};
|
|
|
|
|
2020-05-24 23:13:21 +05:30
|
|
|
return {
|
|
|
|
action,
|
|
|
|
data: omitBy(data, isUndefined),
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
const eventHandler = (e, func, opts = {}) => {
|
2021-04-29 21:17:54 +05:30
|
|
|
const el = e.target.closest('[data-track-event], [data-track-action]');
|
2020-05-24 23:13:21 +05:30
|
|
|
|
|
|
|
if (!el) return;
|
|
|
|
|
|
|
|
const { action, data } = createEventPayload(el, opts);
|
|
|
|
func(opts.category, action, data);
|
2019-12-21 20:55:43 +05:30
|
|
|
};
|
|
|
|
|
|
|
|
const eventHandlers = (category, func) => {
|
2021-03-08 18:12:59 +05:30
|
|
|
const handler = (opts) => (e) => eventHandler(e, func, { ...{ category }, ...opts });
|
2019-12-21 20:55:43 +05:30
|
|
|
const handlers = [];
|
|
|
|
handlers.push({ name: 'click', func: handler() });
|
|
|
|
handlers.push({ name: 'show.bs.dropdown', func: handler({ suffix: '_show' }) });
|
|
|
|
handlers.push({ name: 'hide.bs.dropdown', func: handler({ suffix: '_hide' }) });
|
|
|
|
return handlers;
|
2019-10-12 21:52:04 +05:30
|
|
|
};
|
|
|
|
|
2021-04-17 20:07:23 +05:30
|
|
|
const dispatchEvent = (category = document.body.dataset.page, action = 'generic', data = {}) => {
|
|
|
|
// eslint-disable-next-line @gitlab/require-i18n-strings
|
|
|
|
if (!category) throw new Error('Tracking: no category provided for tracking.');
|
|
|
|
|
2021-09-04 01:27:46 +05:30
|
|
|
const { label, property, value, extra = {} } = data;
|
|
|
|
|
|
|
|
const standardContext = getStandardContext({ extra });
|
|
|
|
const contexts = [standardContext];
|
2021-04-17 20:07:23 +05:30
|
|
|
|
|
|
|
if (data.context) {
|
|
|
|
contexts.push(data.context);
|
|
|
|
}
|
|
|
|
|
|
|
|
return window.snowplow('trackStructEvent', category, action, label, property, value, contexts);
|
|
|
|
};
|
|
|
|
|
2019-10-12 21:52:04 +05:30
|
|
|
export default class Tracking {
|
2021-04-17 20:07:23 +05:30
|
|
|
static queuedEvents = [];
|
|
|
|
static initialized = false;
|
|
|
|
|
2019-12-04 20:38:33 +05:30
|
|
|
static trackable() {
|
|
|
|
return !['1', 'yes'].includes(
|
|
|
|
window.doNotTrack || navigator.doNotTrack || navigator.msDoNotTrack,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-04-17 20:07:23 +05:30
|
|
|
static flushPendingEvents() {
|
|
|
|
this.initialized = true;
|
|
|
|
|
|
|
|
while (this.queuedEvents.length) {
|
|
|
|
dispatchEvent(...this.queuedEvents.shift());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-12 21:52:04 +05:30
|
|
|
static enabled() {
|
2019-12-04 20:38:33 +05:30
|
|
|
return typeof window.snowplow === 'function' && this.trackable();
|
2019-10-12 21:52:04 +05:30
|
|
|
}
|
|
|
|
|
2021-04-17 20:07:23 +05:30
|
|
|
static event(...eventData) {
|
2019-10-12 21:52:04 +05:30
|
|
|
if (!this.enabled()) return false;
|
|
|
|
|
2021-04-17 20:07:23 +05:30
|
|
|
if (!this.initialized) {
|
|
|
|
this.queuedEvents.push(eventData);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return dispatchEvent(...eventData);
|
2019-10-12 21:52:04 +05:30
|
|
|
}
|
|
|
|
|
2020-05-24 23:13:21 +05:30
|
|
|
static bindDocument(category = document.body.dataset.page, parent = document) {
|
|
|
|
if (!this.enabled() || parent.trackingBound) return [];
|
2019-10-12 21:52:04 +05:30
|
|
|
|
2020-05-24 23:13:21 +05:30
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
|
|
parent.trackingBound = true;
|
2019-10-12 21:52:04 +05:30
|
|
|
|
2019-12-21 20:55:43 +05:30
|
|
|
const handlers = eventHandlers(category, (...args) => this.event(...args));
|
2021-03-08 18:12:59 +05:30
|
|
|
handlers.forEach((event) => parent.addEventListener(event.name, event.func));
|
2019-12-21 20:55:43 +05:30
|
|
|
return handlers;
|
2019-10-12 21:52:04 +05:30
|
|
|
}
|
|
|
|
|
2020-05-24 23:13:21 +05:30
|
|
|
static trackLoadEvents(category = document.body.dataset.page, parent = document) {
|
|
|
|
if (!this.enabled()) return [];
|
|
|
|
|
2021-04-29 21:17:54 +05:30
|
|
|
const loadEvents = parent.querySelectorAll(
|
|
|
|
'[data-track-action="render"], [data-track-event="render"]',
|
|
|
|
);
|
2020-05-24 23:13:21 +05:30
|
|
|
|
2021-03-08 18:12:59 +05:30
|
|
|
loadEvents.forEach((element) => {
|
2020-05-24 23:13:21 +05:30
|
|
|
const { action, data } = createEventPayload(element);
|
|
|
|
this.event(category, action, data);
|
|
|
|
});
|
|
|
|
|
|
|
|
return loadEvents;
|
|
|
|
}
|
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
static enableFormTracking(config, contexts = []) {
|
|
|
|
if (!this.enabled()) return;
|
|
|
|
|
2021-09-04 01:27:46 +05:30
|
|
|
if (!Array.isArray(config?.forms?.allow) && !Array.isArray(config?.fields?.allow)) {
|
2021-06-08 01:23:25 +05:30
|
|
|
// eslint-disable-next-line @gitlab/require-i18n-strings
|
2021-09-04 01:27:46 +05:30
|
|
|
throw new Error('Unable to enable form event tracking without allow rules.');
|
2021-06-08 01:23:25 +05:30
|
|
|
}
|
|
|
|
|
2021-09-04 01:27:46 +05:30
|
|
|
// Ignore default/standard schema
|
|
|
|
const standardContext = getStandardContext();
|
|
|
|
const userProvidedContexts = contexts.filter(
|
|
|
|
(context) => context.schema !== standardContext.schema,
|
|
|
|
);
|
|
|
|
|
2021-09-30 23:02:18 +05:30
|
|
|
const mappedConfig = {};
|
|
|
|
if (config.forms) mappedConfig.forms = renameKey(config.forms, 'allow', 'whitelist');
|
|
|
|
if (config.fields) mappedConfig.fields = renameKey(config.fields, 'allow', 'whitelist');
|
2021-09-04 01:27:46 +05:30
|
|
|
|
|
|
|
const enabler = () => window.snowplow('enableFormTracking', mappedConfig, userProvidedContexts);
|
2021-06-08 01:23:25 +05:30
|
|
|
|
2021-09-30 23:02:18 +05:30
|
|
|
if (document.readyState === 'complete') enabler();
|
|
|
|
else {
|
|
|
|
document.addEventListener('readystatechange', () => {
|
|
|
|
if (document.readyState === 'complete') enabler();
|
|
|
|
});
|
|
|
|
}
|
2021-06-08 01:23:25 +05:30
|
|
|
}
|
|
|
|
|
2020-01-01 13:55:28 +05:30
|
|
|
static mixin(opts = {}) {
|
2019-12-21 20:55:43 +05:30
|
|
|
return {
|
2020-01-01 13:55:28 +05:30
|
|
|
computed: {
|
|
|
|
trackingCategory() {
|
|
|
|
const localCategory = this.tracking ? this.tracking.category : null;
|
|
|
|
return localCategory || opts.category;
|
|
|
|
},
|
|
|
|
trackingOptions() {
|
2021-04-29 21:17:54 +05:30
|
|
|
const options = addExperimentContext(opts);
|
|
|
|
return { ...options, ...this.tracking };
|
2020-01-01 13:55:28 +05:30
|
|
|
},
|
2019-12-21 20:55:43 +05:30
|
|
|
},
|
|
|
|
methods: {
|
2020-01-01 13:55:28 +05:30
|
|
|
track(action, data = {}) {
|
|
|
|
const category = data.category || this.trackingCategory;
|
|
|
|
const options = {
|
|
|
|
...this.trackingOptions,
|
|
|
|
...data,
|
|
|
|
};
|
|
|
|
Tracking.event(category, action, options);
|
2019-12-21 20:55:43 +05:30
|
|
|
},
|
|
|
|
},
|
2019-10-12 21:52:04 +05:30
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
2019-12-04 20:38:33 +05:30
|
|
|
|
|
|
|
export function initUserTracking() {
|
|
|
|
if (!Tracking.enabled()) return;
|
|
|
|
|
2019-12-21 20:55:43 +05:30
|
|
|
const opts = { ...DEFAULT_SNOWPLOW_OPTIONS, ...window.snowplowOptions };
|
2019-12-04 20:38:33 +05:30
|
|
|
window.snowplow('newTracker', opts.namespace, opts.hostname, opts);
|
|
|
|
|
2020-11-24 15:15:51 +05:30
|
|
|
document.dispatchEvent(new Event('SnowplowInitialized'));
|
2021-04-17 20:07:23 +05:30
|
|
|
Tracking.flushPendingEvents();
|
2020-11-24 15:15:51 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
export function initDefaultTrackers() {
|
|
|
|
if (!Tracking.enabled()) return;
|
|
|
|
|
2021-09-04 01:27:46 +05:30
|
|
|
const opts = { ...DEFAULT_SNOWPLOW_OPTIONS, ...window.snowplowOptions };
|
|
|
|
|
2019-12-04 20:38:33 +05:30
|
|
|
window.snowplow('enableActivityTracking', 30, 30);
|
2021-04-17 20:07:23 +05:30
|
|
|
// must be after enableActivityTracking
|
2021-09-04 01:27:46 +05:30
|
|
|
const standardContext = getStandardContext();
|
|
|
|
window.snowplow('trackPageView', null, [standardContext]);
|
2019-12-04 20:38:33 +05:30
|
|
|
|
2021-09-04 01:27:46 +05:30
|
|
|
if (window.snowplowOptions.formTracking) Tracking.enableFormTracking(opts.formTrackingConfig);
|
2020-11-24 15:15:51 +05:30
|
|
|
if (window.snowplowOptions.linkClickTracking) window.snowplow('enableLinkClickTracking');
|
2019-12-21 20:55:43 +05:30
|
|
|
|
|
|
|
Tracking.bindDocument();
|
2020-05-24 23:13:21 +05:30
|
|
|
Tracking.trackLoadEvents();
|
2019-12-04 20:38:33 +05:30
|
|
|
}
|