Implement Snowplow tracking

This page describes how to:

  • Implement Snowplow frontend and backend tracking
  • Test Snowplow events

Snowplow JavaScript frontend tracking

GitLab provides a Tracking interface that wraps the Snowplow JavaScript tracker to track custom events.

For the recommended frontend tracking implementation, see Usage recommendations.

Tracking implementations must have an action and a category. You can provide additional categories from the structured event taxonomy with an extra object that accepts key-value pairs.

Field Type Default value Description
category string document.body.dataset.page Page or subsection of a page in which events are captured.
action string generic Action the user is taking. Clicks must be click and activations must be activate. For example, focusing a form field is activate_form_input, and clicking a button is click_button.
data object {} Additional data such as label, property, value, context as described in Structured event taxonomy, and extra (key-value pairs object).

Usage recommendations

  • Use data attributes on HTML elements that emit click, show.bs.dropdown, or hide.bs.dropdown events.
  • Use the Vue mixin for tracking custom events, or if the supported events for data attributes are not propagating.
  • Use the tracking class when tracking raw JavaScript files.

Implement data attribute tracking

To implement tracking for HAML or Vue templates, add a data-track attribute to the element.

The following example shows data-track-* attributes assigned to a button:

%button.btn{ data: { track: { action: "click_button", label: "template_preview", property: "my-template" } } }
<button class="btn"
  data-track-action="click_button"
  data-track-label="template_preview"
  data-track-property="my-template"
  data-track-extra='{ "template_variant": "primary" }'
/>

data-track attributes

Attribute Required Description
data-track-action true Action the user is taking. Clicks must be prepended with click and activations must be prepended with activate. For example, focusing a form field is activate_form_input and clicking a button is click_button. Replaces data-track-event, which was deprecated in GitLab 13.11.
data-track-label false The specific element or object to act on. This can be: the label of the element, for example, a tab labeled ‘Create from template’ for create_from_template; a unique identifier if no text is available, for example, groups_dropdown_close for closing the Groups dropdown in the top bar; or the name or title attribute of a record being created.
data-track-property false Any additional property of the element, or object being acted on.
data-track-value false Describes a numeric value or something directly related to the event. This could be the value of an input. For example, 10 when clicking internal visibility. If omitted, this is the element’s value property or undefined. For checkboxes, the default value is the element’s checked attribute or 0 when unchecked.
data-track-extra false A key-value pair object passed as a valid JSON string. This attribute is added to the extra property in our gitlab_standard schema.
data-track-context false The context as described in our Structured event taxonomy.

Event listeners

Event listeners bind at the document level to handle click events in elements with data attributes. This allows them to be handled when the DOM re-renders or changes. Document-level binding reduces the likelihood that click events stop propagating up the DOM tree.

If click events stop propagating, you must implement listeners and Vue component tracking or raw JavaScript tracking.

Helper methods

Use the following Ruby helper:

tracking_attrs(label, action, property) # { data: { track_label... } }

%button{ **tracking_attrs('main_navigation', 'click_button', 'navigation') }

If you use the GitLab helper method nav_link, you must wrap html_options under the html_options keyword argument. If you use the ActionView helper method link_to, you don’t need to wrap html_options.

# Bad
= nav_link(controller: ['dashboard/groups', 'explore/groups'], data: { track_label: "explore_groups",
track_action: "click_button" })

# Good
= nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { data: { track_label: 
"explore_groups", track_action: "click_button" } })

# Good (other helpers)
= link_to explore_groups_path, title: _("Explore"), data: { track_label: "explore_groups", track_action:
"click_button" }

Implement Vue component tracking

For custom event tracking, use a Vue mixin in components. Vue mixin exposes the Tracking.event static method and the track method called from components or templates. You can specify tracking options in data or computed. These options override any defaults and allow the values to be dynamic from props or based on state.

Default options are passed when an event is tracked from the component. If you don’t specify an option, the default document.body.dataset.page is used. The default options are:

  • category
  • label
  • property
  • value

To implement Vue component tracking:

  1. Import the Tracking library and request a mixin:

     import Tracking from '~/tracking';
     const trackingMixin = Tracking.mixin;
    
  2. Provide categories to track the event from the component. For example, to track all events in a component with a label, use the label category:

     import Tracking from '~/tracking';
     const trackingMixin = Tracking.mixin({ label: 'right_sidebar' });
    
  3. In the component, declare the Vue mixin:

     export default {
       mixins: [trackingMixin],
       // ...[component implementation]...
       data() {
         return {
           expanded: false,
           tracking: {
             label: 'left_sidebar',
           },
         };
       },
     };
    
  4. To receive event data as a tracking object or computed property:
    • Declare it in the data function. Use a tracking object when default event properties are dynamic or provided at runtime:

       export default {
         name: 'RightSidebar',
         mixins: [Tracking.mixin()],
         data() {
           return {
             tracking: {
               label: 'right_sidebar',
               // category: '',
               // property: '',
               // value: '',
               // experiment: '',
               // extra: {},
             },
           };
         },
       };
      
    • Declare it in the event data in the track function. This object merges with any previously provided options:

       this.track('click_button', {
         label: 'right_sidebar',
       });
      
  5. Optional. Use the track method in a template:

     <template>
       <div>
         <button data-testid="toggle" @click="toggle">Toggle</button>
    
         <div v-if="expanded">
           <p>Hello world!</p>
           <button @click="track('click_action')">Track another event</button>
         </div>
       </div>
     </template>
    

The following example shows an implementation of Vue component tracking:

export default {
  name: 'RightSidebar',
  mixins: [Tracking.mixin({ label: 'right_sidebar' })],
  data() {
    return {
      expanded: false,
    };
  },
  methods: {
    toggle() {
      this.expanded = !this.expanded;
      // Additional data will be merged, like `value` below
      this.track('click_toggle', { value: Number(this.expanded) });
    }
  }
};

Testing example

import { mockTracking } from 'helpers/tracking_helper';
// mockTracking(category, documentOverride, spyMethod)

describe('RightSidebar.vue', () => {
  let trackingSpy;
  let wrapper;

  beforeEach(() => {
    trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
  });

  const findToggle = () => wrapper.find('[data-testid="toggle"]');

  it('tracks turning off toggle', () => {
    findToggle().trigger('click');

    expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_toggle', {
      label: 'right_sidebar',
      value: 0,
    });
  });
});

Implement raw JavaScript tracking

To call custom event tracking and instrumentation directly from the JavaScript file, call the Tracking.event static function.

The following example demonstrates tracking a click on a button by manually calling Tracking.event.

import Tracking from '~/tracking';

const button = document.getElementById('create_from_template_button');

button.addEventListener('click', () => {
  Tracking.event('dashboard:projects:index', 'click_button', {
    label: 'create_from_template',
    property: 'template_preview',
    extra: {
      templateVariant: 'primary',
      valid: 1,
    },
  });
});

Testing example

import Tracking from '~/tracking';

describe('MyTracking', () => {
  let wrapper;

  beforeEach(() => {
    jest.spyOn(Tracking, 'event');
  });

  const findButton = () => wrapper.find('[data-testid="create_from_template"]');

  it('tracks event', () => {
    findButton().trigger('click');

    expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
      label: 'create_from_template',
      property: 'template_preview',
      extra: {
        templateVariant: 'primary',
        valid: true,
      },
    });
  });
});

Form tracking

To enable Snowplow automatic form tracking:

  1. Call Tracking.enableFormTracking when the DOM is ready.
  2. Provide a config object that includes at least one of the following elements:
    • forms determines the forms to track. Identified by the CSS class name.
    • fields determines the fields inside the tracked forms to track. Identified by the field name.
  3. Optional. Provide a list of contexts as the second argument. The gitlab_standard schema is excluded from these events.
Tracking.enableFormTracking({
  forms: { allow: ['sign-in-form', 'password-recovery-form'] },
  fields: { allow: ['terms_and_conditions', 'newsletter_agreement'] },
});

Testing example

import Tracking from '~/tracking';

describe('MyFormTracking', () => {
  let formTrackingSpy;

  beforeEach(() => {
    formTrackingSpy = jest
      .spyOn(Tracking, 'enableFormTracking')
      .mockImplementation(() => null);
  });

  it('initialized with the correct configuration', () => {
    expect(formTrackingSpy).toHaveBeenCalledWith({
      forms: { allow: ['sign-in-form', 'password-recovery-form'] },
      fields: { allow: ['terms_and_conditions', 'newsletter_agreement'] },
    });
  });
});

Implement Ruby backend tracking

Gitlab::Tracking is an interface that wraps the Snowplow Ruby Tracker for tracking custom events. Backend tracking provides:

  • User behavior tracking
  • Instrumentation to monitor and visualize performance over time in a section or aspect of code.

To add custom event tracking and instrumentation, call the GitLab::Tracking.event class method. For example:

class Projects::CreateService < BaseService
  def execute
    project = Project.create(params)

    Gitlab::Tracking.event('Projects::CreateService', 'create_project', label: project.errors.full_messages.to_sentence,
                           property: project.valid?.to_s, project: project, user: current_user, namespace: namespace)
  end
end

Use the following arguments:

Argument Type Default value Description
category String   Area or aspect of the application. For example, HealthCheckController or Lfs::FileTransformer.
action String   The action being taken. For example, a controller action such as create, or an Active Record callback.
label String nil The specific element or object to act on. This can be one of the following: the label of the element, for example, a tab labeled ‘Create from template’ for create_from_template; a unique identifier if no text is available, for example, groups_dropdown_close for closing the Groups dropdown in the top bar; or the name or title attribute of a record being created.
property String nil Any additional property of the element, or object being acted on.
value Numeric nil Describes a numeric value or something directly related to the event. This could be the value of an input. For example, 10 when clicking internal visibility.
context Array[SelfDescribingJSON] nil An array of custom contexts to send with this event. Most events should not have any custom contexts.
project Project nil The project associated with the event.
user User nil The user associated with the event.
namespace Namespace nil The namespace associated with the event.
extra Hash {} Additional keyword arguments are collected into a hash and sent with the event.

Unit testing

To test backend Snowplow events, use the expect_snowplow_event helper. For more information, see testing best practices.

Performance

We use the AsyncEmitter when tracking events, which allows for instrumentation calls to be run in a background thread. This is still an active area of development.

Develop and test Snowplow

To develop and test a Snowplow event, there are several tools to test frontend and backend events:

Testing Tool Frontend Tracking Backend Tracking Local Development Environment Production Environment Production Environment
Snowplow Analytics Debugger Chrome Extension Yes No Yes Yes Yes
Snowplow Inspector Chrome Extension Yes No Yes Yes Yes
Snowplow Micro Yes Yes Yes No No

Test frontend events

Before you test frontend events in development, you must:

  1. Enable Snowplow tracking in the Admin Area.
  2. Turn off ad blockers that could prevent Snowplow JavaScript from loading in your environment.
  3. Turn off “Do Not Track” (DNT) in your browser.

All URLs are pseudonymized. The entity identifier replaces personally identifiable information (PII). PII includes usernames, group, and project names.

Snowplow Analytics Debugger Chrome Extension

Snowplow Analytics Debugger is a browser extension for testing frontend events. It works in production, staging, and local development environments.

  1. Install the Snowplow Analytics Debugger Chrome browser extension.
  2. Open Chrome DevTools to the Snowplow Analytics Debugger tab.

Snowplow Inspector Chrome Extension

Snowplow Inspector Chrome Extension is a browser extension for testing frontend events. This works in production, staging, and local development environments.

  1. Install Snowplow Inspector.
  2. To open the extension, select the Snowplow Inspector icon beside the address bar.
  3. Click around on a webpage with Snowplow to see JavaScript events firing in the inspector window.

Test backend events

Snowplow Micro

Snowplow Micro is a Docker-based solution for testing backend and frontend in a local development environment. Snowplow Micro records the same events as the full Snowplow pipeline. To query events, use the Snowplow Micro API.

To install and run Snowplow Micro, complete these steps to modify the GitLab Development Kit (GDK):

  1. Ensure Docker is installed and running.

  2. To install Snowplow Micro, clone the settings in this project.

  3. Navigate to the directory with the cloned project, and start the appropriate Docker container:

    ./snowplow-micro.sh
    
  4. Use GDK to start the PostgreSQL terminal and connect to the gitlabhq_development database:

    gdk psql -d gitlabhq_development
    
  5. Update your instance’s settings to enable Snowplow events and point to the Snowplow Micro collector:

    update application_settings set snowplow_collector_hostname='localhost:9090', snowplow_enabled=true, snowplow_cookie_domain='.gitlab.com';
    
  6. Update DEFAULT_SNOWPLOW_OPTIONS in app/assets/javascripts/tracking/constants.js to remove forceSecureTracker: true:

    diff --git a/app/assets/javascripts/tracking/constants.js b/app/assets/javascripts/tracking/constants.js
    index 598111e4086..eff38074d4c 100644
    --- a/app/assets/javascripts/tracking/constants.js
    +++ b/app/assets/javascripts/tracking/constants.js
    @@ -7,7 +7,6 @@ export const DEFAULT_SNOWPLOW_OPTIONS = {
       appId: '',
       userFingerprint: false,
       respectDoNotTrack: true,
    -  forceSecureTracker: true,
       eventMethod: 'post',
       contexts: { webPage: true, performanceTiming: true },
       formTracking: false,
    
  7. Update options in lib/gitlab/tracking.rb to add protocol and port:

    diff --git a/lib/gitlab/tracking.rb b/lib/gitlab/tracking.rb
    index 618e359211b..e9084623c43 100644
    --- a/lib/gitlab/tracking.rb
    +++ b/lib/gitlab/tracking.rb
    @@ -41,7 +41,9 @@ def options(group)
               cookie_domain: Gitlab::CurrentSettings.snowplow_cookie_domain,
               app_id: Gitlab::CurrentSettings.snowplow_app_id,
               form_tracking: additional_features,
    -          link_click_tracking: additional_features
    +          link_click_tracking: additional_features,
    +          protocol: 'http',
    +          port: 9090
             }.transform_keys! { |key| key.to_s.camelize(:lower).to_sym }
           end
    
  8. Update emitter in lib/gitlab/tracking/destinations/snowplow.rb to change protocol:

    diff --git a/lib/gitlab/tracking/destinations/snowplow.rb b/lib/gitlab/tracking/destinations/snowplow.rb
    index 4fa844de325..5dd9d0eacfb 100644
    --- a/lib/gitlab/tracking/destinations/snowplow.rb
    +++ b/lib/gitlab/tracking/destinations/snowplow.rb
    @@ -40,7 +40,7 @@ def tracker
             def emitter
               SnowplowTracker::AsyncEmitter.new(
                 Gitlab::CurrentSettings.snowplow_collector_hostname,
    -            protocol: 'https'
    +            protocol: 'http'
               )
             end
           end
    
    
  9. Restart GDK:

    gdk restart
    
  10. Send a test Snowplow event from the Rails console:

    Gitlab::Tracking.event('category', 'action')
    
  11. Navigate to localhost:9090/micro/good to see the event.

Troubleshoot

To control content security policy warnings when using an external host, modify config/gitlab.yml to allow or disallow them. To allow them, add the relevant host for connect_src. For example, for https://snowplow.trx.gitlab.net:

development:
  <<: *base
  gitlab:
    content_security_policy:
      enabled: true
      directives:
        connect_src: "'self' http://localhost:* http://127.0.0.1:* ws://localhost:* wss://localhost:* ws://127.0.0.1:* https://snowplow.trx.gitlab.net/"