summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/tracking.js
blob: 7c0097fbe371e774d9ac396d51f8b054ba206b51 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import _ from 'underscore';

const DEFAULT_SNOWPLOW_OPTIONS = {
  namespace: 'gl',
  hostname: window.location.hostname,
  cookieDomain: window.location.hostname,
  appId: '',
  userFingerprint: false,
  respectDoNotTrack: true,
  forceSecureTracker: true,
  eventMethod: 'post',
  contexts: { webPage: true },
  formTracking: false,
  linkClickTracking: false,
};

const eventHandler = (e, func, opts = {}) => {
  const el = e.target.closest('[data-track-event]');
  const action = el && el.dataset.trackEvent;
  if (!action) return;

  let value = el.dataset.trackValue || el.value || undefined;
  if (el.type === 'checkbox' && !el.checked) value = false;

  const data = {
    label: el.dataset.trackLabel,
    property: el.dataset.trackProperty,
    value,
    context: el.dataset.trackContext,
  };

  func(opts.category, action + (opts.suffix || ''), _.omit(data, _.isUndefined));
};

const eventHandlers = (category, func) => {
  const handler = opts => e => eventHandler(e, func, { ...{ category }, ...opts });
  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;
};

export default class Tracking {
  static trackable() {
    return !['1', 'yes'].includes(
      window.doNotTrack || navigator.doNotTrack || navigator.msDoNotTrack,
    );
  }

  static enabled() {
    return typeof window.snowplow === 'function' && this.trackable();
  }

  static event(category = document.body.dataset.page, action = 'generic', data = {}) {
    if (!this.enabled()) return false;
    // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
    if (!category) throw new Error('Tracking: no category provided for tracking.');

    const { label, property, value, context } = data;
    const contexts = context ? [context] : undefined;
    return window.snowplow('trackStructEvent', category, action, label, property, value, contexts);
  }

  static bindDocument(category = document.body.dataset.page, documentOverride = null) {
    const el = documentOverride || document;
    if (!this.enabled() || el.trackingBound) return [];

    el.trackingBound = true;

    const handlers = eventHandlers(category, (...args) => this.event(...args));
    handlers.forEach(event => el.addEventListener(event.name, event.func));
    return handlers;
  }

  static mixin(opts) {
    return {
      data() {
        return {
          tracking: {
            // eslint-disable-next-line no-underscore-dangle
            category: this.$options.name || this.$options._componentTag,
          },
        };
      },
      methods: {
        track(action, data) {
          const category = opts.category || data.category || this.tracking.category;
          Tracking.event(category || 'unspecified', action, { ...opts, ...this.tracking, ...data });
        },
      },
    };
  }
}

export function initUserTracking() {
  if (!Tracking.enabled()) return;

  const opts = { ...DEFAULT_SNOWPLOW_OPTIONS, ...window.snowplowOptions };
  window.snowplow('newTracker', opts.namespace, opts.hostname, opts);

  window.snowplow('enableActivityTracking', 30, 30);
  window.snowplow('trackPageView'); // must be after enableActivityTracking

  if (opts.formTracking) window.snowplow('enableFormTracking');
  if (opts.linkClickTracking) window.snowplow('enableLinkClickTracking');

  Tracking.bindDocument();
}