Commit 9474ea6d authored by Jacques Erasmus's avatar Jacques Erasmus

Merge branch '356621-fe-dispatch-snowplow-events-from-their-event-definitions' into 'master'

FE - Dispatch Snowplow events from their event definitions

See merge request gitlab-org/gitlab!84122
parents c114931e 24a6b0f5
......@@ -13,8 +13,12 @@ import {
const ALLOWED_URL_HASHES = ['#diff', '#note'];
export default class Tracking {
static queuedEvents = [];
static nonInitializedQueue = [];
static initialized = false;
static definitionsLoaded = false;
static definitionsManifest = {};
static definitionsEventsQueue = [];
static definitions = [];
/**
* (Legacy) Determines if tracking is enabled at the user level.
......@@ -54,13 +58,71 @@ export default class Tracking {
}
if (!this.initialized) {
this.queuedEvents.push(eventData);
this.nonInitializedQueue.push(eventData);
return false;
}
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.
*
......@@ -69,8 +131,8 @@ export default class Tracking {
static flushPendingEvents() {
this.initialized = true;
while (this.queuedEvents.length) {
dispatchSnowplowEvent(...this.queuedEvents.shift());
while (this.nonInitializedQueue.length) {
dispatchSnowplowEvent(...this.nonInitializedQueue.shift());
}
}
......
......@@ -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', () => {
it('tells snowplow to enable form tracking, with only explicit contexts', () => {
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