Commit 24a6b0f5 authored by Axel García's avatar Axel García Committed by Jacques Erasmus

Fire a Snowplow events with its definition on FE

Changelog: added
parent 570d2628
...@@ -13,8 +13,12 @@ import { ...@@ -13,8 +13,12 @@ import {
const ALLOWED_URL_HASHES = ['#diff', '#note']; const ALLOWED_URL_HASHES = ['#diff', '#note'];
export default class Tracking { export default class Tracking {
static queuedEvents = []; static nonInitializedQueue = [];
static initialized = false; static initialized = false;
static definitionsLoaded = false;
static definitionsManifest = {};
static definitionsEventsQueue = [];
static definitions = [];
/** /**
* (Legacy) Determines if tracking is enabled at the user level. * (Legacy) Determines if tracking is enabled at the user level.
...@@ -54,13 +58,71 @@ export default class Tracking { ...@@ -54,13 +58,71 @@ export default class Tracking {
} }
if (!this.initialized) { if (!this.initialized) {
this.queuedEvents.push(eventData); this.nonInitializedQueue.push(eventData);
return false; return false;
} }
return dispatchSnowplowEvent(...eventData); return dispatchSnowplowEvent(...eventData);
} }
/**
* Preloads event definitions.
*
* @returns {undefined}
*/
static loadDefinitions() {
// TODO: fetch definitions from the server and flush the queue
// See https://gitlab.com/gitlab-org/gitlab/-/issues/358256
this.definitionsLoaded = true;
while (this.definitionsEventsQueue.length) {
this.dispatchFromDefinition(...this.definitionsEventsQueue.shift());
}
}
/**
* Dispatches a structured event with data from its event definition.
*
* @param {String} basename
* @param {Object} eventData
* @returns {undefined|Boolean}
*/
static definition(basename, eventData = {}) {
if (!this.enabled()) {
return false;
}
if (!(basename in this.definitionsManifest)) {
throw new Error(`Missing Snowplow event definition "${basename}"`);
}
return this.dispatchFromDefinition(basename, eventData);
}
/**
* Builds an event with data from a valid definition and sends it to
* Snowplow. If the definitions are not loaded, it pushes the data to a queue.
*
* @param {String} basename
* @param {Object} eventData
* @returns {undefined|Boolean}
*/
static dispatchFromDefinition(basename, eventData) {
if (!this.definitionsLoaded) {
this.definitionsEventsQueue.push([basename, eventData]);
return false;
}
const eventDefinition = this.definitions.find((definition) => definition.key === basename);
return this.event(
eventData.category ?? eventDefinition.category,
eventData.action ?? eventDefinition.action,
eventData,
);
}
/** /**
* Dispatches any event emitted before initialization. * Dispatches any event emitted before initialization.
* *
...@@ -69,8 +131,8 @@ export default class Tracking { ...@@ -69,8 +131,8 @@ export default class Tracking {
static flushPendingEvents() { static flushPendingEvents() {
this.initialized = true; this.initialized = true;
while (this.queuedEvents.length) { while (this.nonInitializedQueue.length) {
dispatchSnowplowEvent(...this.queuedEvents.shift()); dispatchSnowplowEvent(...this.nonInitializedQueue.shift());
} }
} }
......
...@@ -129,6 +129,72 @@ describe('Tracking', () => { ...@@ -129,6 +129,72 @@ describe('Tracking', () => {
}); });
}); });
describe('.definition', () => {
const TEST_VALID_BASENAME = '202108302307_default_click_button';
const TEST_EVENT_DATA = { category: undefined, action: 'click_button' };
let eventSpy;
let dispatcherSpy;
beforeAll(() => {
Tracking.definitionsManifest = {
'202108302307_default_click_button': 'config/events/202108302307_default_click_button.yml',
};
});
beforeEach(() => {
eventSpy = jest.spyOn(Tracking, 'event');
dispatcherSpy = jest.spyOn(Tracking, 'dispatchFromDefinition');
});
it('throws an error if the definition does not exists', () => {
const basename = '20220230_default_missing_definition';
const expectedError = new Error(`Missing Snowplow event definition "${basename}"`);
expect(() => Tracking.definition(basename)).toThrow(expectedError);
});
it('dispatches an event from a definition present in the manifest', () => {
Tracking.definition(TEST_VALID_BASENAME);
expect(dispatcherSpy).toHaveBeenCalledWith(TEST_VALID_BASENAME, {});
});
it('push events to the queue if not loaded', () => {
Tracking.definitionsLoaded = false;
Tracking.definitionsEventsQueue = [];
const dispatched = Tracking.definition(TEST_VALID_BASENAME);
expect(dispatched).toBe(false);
expect(Tracking.definitionsEventsQueue[0]).toStrictEqual([TEST_VALID_BASENAME, {}]);
expect(eventSpy).not.toHaveBeenCalled();
});
it('dispatch events when the definition is loaded', () => {
const definition = { key: TEST_VALID_BASENAME, ...TEST_EVENT_DATA };
Tracking.definitions = [{ ...definition }];
Tracking.definitionsEventsQueue = [];
Tracking.definitionsLoaded = true;
const dispatched = Tracking.definition(TEST_VALID_BASENAME);
expect(dispatched).not.toBe(false);
expect(Tracking.definitionsEventsQueue).toEqual([]);
expect(eventSpy).toHaveBeenCalledWith(definition.category, definition.action, {});
});
it('lets defined event data takes precedence', () => {
const definition = { key: TEST_VALID_BASENAME, category: undefined, action: 'click_button' };
const eventData = { category: TEST_CATEGORY };
Tracking.definitions = [{ ...definition }];
Tracking.definitionsLoaded = true;
Tracking.definition(TEST_VALID_BASENAME, eventData);
expect(eventSpy).toHaveBeenCalledWith(TEST_CATEGORY, definition.action, eventData);
});
});
describe('.enableFormTracking', () => { describe('.enableFormTracking', () => {
it('tells snowplow to enable form tracking, with only explicit contexts', () => { it('tells snowplow to enable form tracking, with only explicit contexts', () => {
const config = { forms: { allow: ['form-class1'] }, fields: { allow: ['input-class1'] } }; const config = { forms: { allow: ['form-class1'] }, fields: { allow: ['input-class1'] } };
......
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