Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/tracking/tracking.js')
-rw-r--r--app/assets/javascripts/tracking/tracking.js193
1 files changed, 193 insertions, 0 deletions
diff --git a/app/assets/javascripts/tracking/tracking.js b/app/assets/javascripts/tracking/tracking.js
new file mode 100644
index 00000000000..a1f745bc172
--- /dev/null
+++ b/app/assets/javascripts/tracking/tracking.js
@@ -0,0 +1,193 @@
+import { LOAD_ACTION_ATTR_SELECTOR, DEPRECATED_LOAD_EVENT_ATTR_SELECTOR } from './constants';
+import { dispatchSnowplowEvent } from './dispatch_snowplow_event';
+import getStandardContext from './get_standard_context';
+import { getEventHandlers, createEventPayload, renameKey, addExperimentContext } from './utils';
+
+export default class Tracking {
+ static queuedEvents = [];
+ static initialized = false;
+
+ /**
+ * (Legacy) Determines if tracking is enabled at the user level.
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/DNT.
+ *
+ * @returns {Boolean}
+ */
+ static trackable() {
+ return !['1', 'yes'].includes(
+ window.doNotTrack || navigator.doNotTrack || navigator.msDoNotTrack,
+ );
+ }
+
+ /**
+ * Determines if Snowplow is available/enabled.
+ *
+ * @returns {Boolean}
+ */
+ static enabled() {
+ return typeof window.snowplow === 'function' && this.trackable();
+ }
+
+ /**
+ * Dispatches a structured event per our taxonomy:
+ * https://docs.gitlab.com/ee/development/snowplow/index.html#structured-event-taxonomy.
+ *
+ * If the library is not initialized and events are trying to be
+ * dispatched (data-attributes, load-events), they will be added
+ * to a queue to be flushed afterwards.
+ *
+ * @param {...any} eventData defined event taxonomy
+ * @returns {undefined|Boolean}
+ */
+ static event(...eventData) {
+ if (!this.enabled()) {
+ return false;
+ }
+
+ if (!this.initialized) {
+ this.queuedEvents.push(eventData);
+ return false;
+ }
+
+ return dispatchSnowplowEvent(...eventData);
+ }
+
+ /**
+ * Dispatches any event emitted before initialization.
+ *
+ * @returns {undefined}
+ */
+ static flushPendingEvents() {
+ this.initialized = true;
+
+ while (this.queuedEvents.length) {
+ dispatchSnowplowEvent(...this.queuedEvents.shift());
+ }
+ }
+
+ /**
+ * Attaches event handlers for data-attributes powered events.
+ *
+ * @param {String} category - the default category for all events
+ * @param {HTMLElement} parent - element containing data-attributes
+ * @returns {Array}
+ */
+ static bindDocument(category = document.body.dataset.page, parent = document) {
+ if (!this.enabled() || parent.trackingBound) {
+ return [];
+ }
+
+ // eslint-disable-next-line no-param-reassign
+ parent.trackingBound = true;
+
+ const handlers = getEventHandlers(category, (...args) => this.event(...args));
+ handlers.forEach((event) => parent.addEventListener(event.name, event.func));
+
+ return handlers;
+ }
+
+ /**
+ * Attaches event handlers for load-events (on render).
+ *
+ * @param {String} category - the default category for all events
+ * @param {HTMLElement} parent - element containing event targets
+ * @returns {Array}
+ */
+ static trackLoadEvents(category = document.body.dataset.page, parent = document) {
+ if (!this.enabled()) {
+ return [];
+ }
+
+ const loadEvents = parent.querySelectorAll(
+ `${LOAD_ACTION_ATTR_SELECTOR}, ${DEPRECATED_LOAD_EVENT_ATTR_SELECTOR}`,
+ );
+
+ loadEvents.forEach((element) => {
+ const { action, data } = createEventPayload(element);
+ this.event(category, action, data);
+ });
+
+ return loadEvents;
+ }
+
+ /**
+ * Enable Snowplow automatic form tracking.
+ * The config param requires at least one array of either forms
+ * class names, or field name attributes.
+ * https://docs.gitlab.com/ee/development/snowplow/index.html#form-tracking.
+ *
+ * @param {Object} config
+ * @param {Array} contexts
+ * @returns {undefined}
+ */
+ static enableFormTracking(config, contexts = []) {
+ if (!this.enabled()) {
+ return;
+ }
+
+ if (!Array.isArray(config?.forms?.allow) && !Array.isArray(config?.fields?.allow)) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Unable to enable form event tracking without allow rules.');
+ }
+
+ // Ignore default/standard schema
+ const standardContext = getStandardContext();
+ const userProvidedContexts = contexts.filter(
+ (context) => context.schema !== standardContext.schema,
+ );
+
+ const mappedConfig = {};
+ if (config.forms) {
+ mappedConfig.forms = renameKey(config.forms, 'allow', 'whitelist');
+ }
+
+ if (config.fields) {
+ mappedConfig.fields = renameKey(config.fields, 'allow', 'whitelist');
+ }
+
+ const enabler = () => window.snowplow('enableFormTracking', mappedConfig, userProvidedContexts);
+
+ if (document.readyState === 'complete') {
+ enabler();
+ } else {
+ document.addEventListener('readystatechange', () => {
+ if (document.readyState === 'complete') {
+ enabler();
+ }
+ });
+ }
+ }
+
+ /**
+ * Returns an implementation of this class in the form of
+ * a Vue mixin.
+ *
+ * @param {Object} opts - default options for all events
+ * @returns {Object}
+ */
+ static mixin(opts = {}) {
+ return {
+ computed: {
+ trackingCategory() {
+ const localCategory = this.tracking ? this.tracking.category : null;
+ return localCategory || opts.category;
+ },
+ trackingOptions() {
+ const options = addExperimentContext(opts);
+ return { ...options, ...this.tracking };
+ },
+ },
+ methods: {
+ track(action, data = {}) {
+ const category = data.category || this.trackingCategory;
+ const options = {
+ ...this.trackingOptions,
+ ...data,
+ };
+
+ Tracking.event(category, action, options);
+ },
+ },
+ };
+ }
+}