diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-19 12:08:42 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-19 12:08:42 +0300 |
commit | b76ae638462ab0f673e5915986070518dd3f9ad3 (patch) | |
tree | bdab0533383b52873be0ec0eb4d3c66598ff8b91 /app/assets/javascripts/tracking/tracking.js | |
parent | 434373eabe7b4be9593d18a585fb763f1e5f1a6f (diff) |
Add latest changes from gitlab-org/gitlab@14-2-stable-eev14.2.0-rc42
Diffstat (limited to 'app/assets/javascripts/tracking/tracking.js')
-rw-r--r-- | app/assets/javascripts/tracking/tracking.js | 193 |
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); + }, + }, + }; + } +} |