Commit 23cc2460 authored by Jeremy Jackson's avatar Jeremy Jackson Committed by Mike Greiling

Adds new tracking interface for snowplow

This will ultimately replace the stats.js that
exists in EE.
parent d89d71eb
import $ from 'jquery';
const extractData = (el, opts = {}) => {
const { trackEvent, trackLabel = '', trackProperty = '' } = el.dataset;
let trackValue = el.dataset.trackValue || el.value || '';
if (el.type === 'checkbox' && !el.checked) trackValue = false;
return [
trackEvent + (opts.suffix || ''),
{
label: trackLabel,
property: trackProperty,
value: trackValue,
},
];
};
export default class Tracking {
static enabled() {
return typeof window.snowplow === 'function';
}
static event(category = document.body.dataset.page, event = '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.');
return window.snowplow(
'trackStructEvent',
category,
event,
Object.assign({}, { label: '', property: '', value: '' }, data),
);
}
constructor(category = document.body.dataset.page) {
this.category = category;
}
bind(container = document) {
if (!this.constructor.enabled()) return;
container.querySelectorAll(`[data-track-event]`).forEach(el => {
if (this.customHandlingFor(el)) return;
// jquery is required for select2, so we use it always
// see: https://github.com/select2/select2/issues/4686
$(el).on('click', this.eventHandler(this.category));
});
}
customHandlingFor(el) {
const classes = el.classList;
// bootstrap dropdowns
if (classes.contains('dropdown')) {
$(el).on('show.bs.dropdown', this.eventHandler(this.category, { suffix: '_show' }));
$(el).on('hide.bs.dropdown', this.eventHandler(this.category, { suffix: '_hide' }));
return true;
}
return false;
}
eventHandler(category = null, opts = {}) {
return e => {
this.constructor.event(category || this.category, ...extractData(e.currentTarget, opts));
};
}
}
---
title: Moves snowplow tracking from ee to ce
merge_request: 31160
author: jejacks0n
type: added
# Event Tracking
We use [Snowplow](https://github.com/snowplow/snowplow) for tracking custom events (available in GitLab [Enterprise Edition](https://about.gitlab.com/pricing/) only).
We use a tracking interface that wraps up [Snowplow](https://github.com/snowplow/snowplow) for tracking custom events. Snowplow implements page tracking, but also exposes custom event tracking.
## Generic tracking function
In addition to Snowplow's built-in method for tracking page views, we use a generic tracking function which enables us to selectively apply listeners to events.
The generic tracking function can be imported in EE-specific JS files as follows:
The tracking interface can be imported in JS files as follows:
```javascript
import { trackEvent } from `ee/stats`;
import Tracking from `~/tracking`;
```
This gives the user access to the `trackEvent` method, which takes the following parameters:
## Tracking in HAML or Vue templates
| parameter | type | description | required |
| ---------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `category` | string | Describes the page that you're capturing click events on. Unless infeasible, please use the Rails page attribute `document.body.dataset.page` by default. | true |
| `eventName` | string | Describes the action the user is taking. The first word should always describe the action. For example, clicks should be `click` and activations should be `activate`. Use underscores to describe what was acted on. For example, activating a form field would be `activate_form_input`. Clicking on a dropdown is `click_dropdown`. | true |
| `additionalData` | object | Additional data such as `label`, `property`, and `value` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/product/feature-instrumentation/#taxonomy). | false |
To avoid having to do create a bunch of custom javascript event handlers, when working within HAML or Vue templates, we can add `data-track-*` attributes to elements of interest. This way, all elements that have a `data-track-event` attribute to automatically have event tracking bound.
Read more about instrumentation and the taxonomy in the [Product Handbook](https://about.gitlab.com/handbook/product/feature-instrumentation).
### Tracking in `.js` and `.vue` files
Below is an example of `data-track-*` attributes assigned to a button in HAML:
The most simple use case is to add tracking programmatically to an event of interest in Javascript.
```haml
%button.btn{ data: { track_event: "click_button", track_label: "template_preview", track_property: "my-template", track_value: "" } }
```
The following example demonstrates how to track a click on a button in Javascript by calling the `trackEvent` method explicitly:
We can then setup tracking for large sections of a page, or an entire page by telling the Tracking interface to bind to it.
```javascript
import { trackEvent } from `ee/stats`;
import Tracking from '~/tracking';
trackEvent('dashboard:projects:index', 'click_button', {
label: 'create_from_template',
property: 'template_preview',
value: 'rails',
// for the entire document
new Tracking().bind();
// for a container element
document.addEventListener('DOMContentLoaded', () => {
new Tracking('my_category').bind(document.getElementById('my-container'));
});
```
### Tracking in HAML templates
When you instantiate a Tracking instance you can provide a category. If none is provided, `document.body.dataset.page` will be used. When you bind the Tracking instance you can provide an element. If no element is provided to bind to, the `document` is assumed.
Sometimes we want to track clicks for multiple elements on a page. Creating event handlers for all elements could soon turn into a tedious task.
Below is a list of supported `data-track-*` attributes:
There's a more convenient solution to this problem. When working with HAML templates, we can add `data-track-*` attributes to elements of interest. This way, all elements that have both `data-track-label` and `data-track-event` attributes assigned get marked for event tracking. All we have to do is call the `bindTrackableContainer` method on a container which allows for better scoping.
| attribute | required | description |
|:----------------------|:---------|:------------|
| `data-track-event` | true | Action the user is taking. Clicks should be `click` and activations should be `activate`, so for example, focusing a form field would be `activate_form_input`, and clicking a button would be `click_button`. |
| `data-track-label` | false | The `label` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/product/feature-instrumentation/#taxonomy) |
| `data-track-property` | false | The `property` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/product/feature-instrumentation/#taxonomy)
| `data-track-value` | false | The `value` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/product/feature-instrumentation/#taxonomy). If omitted, this will be the elements `value` property or an empty string. For checkboxes, the default value will be the element's checked attribute or `false` when unchecked.
Below is an example of `data-track-*` attributes assigned to a button in HAML:
```ruby
%button.btn{ data: { track_label: "template_preview", track_property: "my-template", track_event: "click_button", track_value: "" } }
```
By calling `bindTrackableContainer('.my-container')`, click handlers get bound to all elements located in `.my-container` provided that they have the necessary `data-track-*` attributes assigned to them.
## Tracking in raw Javascript
```javascript
import Stats from 'ee/stats';
Custom events can be tracked by directly calling the `Tracking.event` static function, which accepts the following arguments:
document.addEventListener('DOMContentLoaded', () => {
Stats.bindTrackableContainer('.my-container', 'category');
});
```
| argument | type | default value | description |
|:-----------|:-------|:---------------------------|:------------|
| `category` | string | document.body.dataset.page | Page or subsection of a page that events are being captured within. |
| `event` | string | 'generic' | Action the user is taking. Clicks should be `click` and activations should be `activate`, so for example, focusing a form field would be `activate_form_input`, and clicking a button would be `click_button`. |
| `data` | object | {} | Additional data such as `label`, `property`, and `value` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/product/feature-instrumentation/#taxonomy). These will be set as empty strings if you don't provide them. |
The second parameter in `bindTrackableContainer` is optional. If omitted, the value of `document.body.dataset.page` will be used as category instead.
Tracking can be programmatically added to an event of interest in Javascript, and the following example demonstrates tracking a click on a button by calling `Tracking.event` manually.
Below is a list of supported `data-track-*` attributes:
```javascript
import Tracking from `~/tracking`;
| attribute | description | required |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `data-track-label` | The `label` in `trackEvent` | true |
| `data-track-event` | The `eventName` in `trackEvent` | true |
| `data-track-property` | The `property` in `trackEvent`. If omitted, an empty string will be used as a default value. | false |
| `data-track-value` | The `value` in `trackEvent`. If omitted, this will be `target.value` or empty string. For checkboxes, the default value being tracked will be the element's checked attribute if `data-track-value` is omitted. | false |
document.getElementById('my_button').addEventListener('click', () => {
Tracking.event('dashboard:projects:index', 'click_button', {
label: 'create_from_template',
property: 'template_preview',
value: 'rails',
});
})
```
Since Snowplow is an Enterprise Edition feature, it's necessary to create a CE backport when adding `data-track-*` attributes to HAML templates in most cases.
## Testing
## Toggling tracking on or off
Snowplow can be enabled by navigating to:
......
import $ from 'jquery';
import { setHTMLFixture } from './helpers/fixtures';
import Tracking from '~/tracking';
describe('Tracking', () => {
beforeEach(() => {
window.snowplow = window.snowplow || (() => {});
});
describe('.event', () => {
let snowplowSpy = null;
beforeEach(() => {
snowplowSpy = jest.spyOn(window, 'snowplow');
});
it('tracks to snowplow (our current tracking system)', () => {
Tracking.event('_category_', '_eventName_', { label: '_label_' });
expect(snowplowSpy).toHaveBeenCalledWith('trackStructEvent', '_category_', '_eventName_', {
label: '_label_',
property: '',
value: '',
});
});
it('skips tracking if snowplow is unavailable', () => {
window.snowplow = false;
Tracking.event('_category_', '_eventName_');
expect(snowplowSpy).not.toHaveBeenCalled();
});
it('skips tracking if ', () => {
window.snowplow = false;
Tracking.event('_category_', '_eventName_');
expect(snowplowSpy).not.toHaveBeenCalled();
});
});
describe('tracking interface events', () => {
let eventSpy = null;
let subject = null;
beforeEach(() => {
eventSpy = jest.spyOn(Tracking, 'event');
subject = new Tracking('_category_');
setHTMLFixture(`
<input data-track-event="click_input1" data-track-label="_label_" value="_value_"/>
<input data-track-event="click_input2" data-track-value="_value_override_" value="_value_"/>
<input type="checkbox" data-track-event="toggle_checkbox" value="_value_" checked/>
<input class="dropdown" data-track-event="toggle_dropdown"/>
<div class="js-projects-list-holder"></div>
`);
});
it('binds to clicks on elements matching [data-track-event]', () => {
subject.bind(document);
$('[data-track-event="click_input1"]').click();
expect(eventSpy).toHaveBeenCalledWith('_category_', 'click_input1', {
label: '_label_',
value: '_value_',
property: '',
});
});
it('allows value override with the data-track-value attribute', () => {
subject.bind(document);
$('[data-track-event="click_input2"]').click();
expect(eventSpy).toHaveBeenCalledWith('_category_', 'click_input2', {
label: '',
value: '_value_override_',
property: '',
});
});
it('handles checkbox values correctly', () => {
subject.bind(document);
const $checkbox = $('[data-track-event="toggle_checkbox"]');
$checkbox.click(); // unchecking
expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_checkbox', {
label: '',
property: '',
value: false,
});
$checkbox.click(); // checking
expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_checkbox', {
label: '',
property: '',
value: '_value_',
});
});
it('handles bootstrap dropdowns', () => {
new Tracking('_category_').bind(document);
const $dropdown = $('[data-track-event="toggle_dropdown"]');
$dropdown.trigger('show.bs.dropdown'); // showing
expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_show', {
label: '',
property: '',
value: '',
});
$dropdown.trigger('hide.bs.dropdown'); // hiding
expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_hide', {
label: '',
property: '',
value: '',
});
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment