Skip to content

Commit

Permalink
feat: Track events in GA (#171)
Browse files Browse the repository at this point in the history
Track commercial events in GA
  • Loading branch information
ioanna0 authored Feb 22, 2021
1 parent 6a8fb1c commit f868615
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 8 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ node_modules
dist
coverage
.DS_Store
.idea
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@semantic-release/github": "7.2.0",
"@types/doubleclick-gpt": "^2019041801.0.4",
"@types/jest": "^26.0.20",
"@types/google.analytics": "^0.0.41",
"@typescript-eslint/eslint-plugin": "^4.15.1",
"@typescript-eslint/parser": "^4.15.1",
"commitizen": "^4.2.3",
Expand Down
81 changes: 81 additions & 0 deletions src/EventTimer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { EventTimer } from './EventTimer';
import { trackEvent } from './GoogleAnalytics';

jest.mock('./GoogleAnalytics');

describe('EventTimer', () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- delete performance to mock it as is readonly
delete (window as any).performance;

const performance = {
now: jest.fn(),
mark: jest.fn(),
getEntriesByName: jest.fn().mockReturnValue([
{
duration: 1,
entryType: 'mark',
name: 'commercial event',
startTime: 1,
},
]),
};

Object.defineProperty(window, 'performance', {
configurable: true,
enumerable: true,
value: performance,
writable: true,
});

beforeEach(() => {
window.guardian = {
config: {
googleAnalytics: {
trackers: {
editorial: 'gaTrackerTest',
},
},
},
};

EventTimer.init();
});

it('trigger first slotReady event', () => {
const eventTimer = EventTimer.get();
eventTimer.trigger('slotReady', 'inline1');
// eslint-disable-next-line @typescript-eslint/unbound-method -- for test
expect(window.performance.mark).toHaveBeenCalledWith(
'gu.commercial.first-slotReady',
);
expect(trackEvent).toHaveBeenCalledWith(
'gu.commercial.slotReady',
'first-slotReady',
'new',
);
});

it('trigger top-above-nav slotReady event', () => {
const eventTimer = EventTimer.get();
eventTimer.trigger('slotReady', 'top-above-nav');
// eslint-disable-next-line @typescript-eslint/unbound-method -- for test
expect(window.performance.mark).toHaveBeenCalledWith(
'gu.commercial.top-above-nav-slotReady',
);
expect(trackEvent).toHaveBeenCalledWith(
'gu.commercial.slotReady',
'top-above-nav-slotReady',
'new',
);
});

it('not trigger a GA event if not in GA config', () => {
const eventTimer = EventTimer.get();
eventTimer.trigger('adOnPage', 'inline1');
// eslint-disable-next-line @typescript-eslint/unbound-method -- for test
expect(window.performance.mark).toHaveBeenCalledWith(
'gu.commercial.first-adOnPage',
);
expect(trackEvent).not.toHaveBeenCalled();
});
});
51 changes: 43 additions & 8 deletions src/EventTimer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { trackEvent } from './GoogleAnalytics';

class Event {
name: string;
ts: DOMHighResTimeStamp;
Expand All @@ -7,6 +9,14 @@ class Event {
this.ts = mark.startTime;
}
}
interface GALogEvent {
name: string;
label: string;
}

interface GAConfig {
logEvents: GALogEvent[];
}

interface SlotEventStatus {
prebidStart: boolean;
Expand All @@ -23,7 +33,7 @@ export class EventTimer {
first: SlotEventStatus;
'top-above-nav': SlotEventStatus;
};

gaConfig: GAConfig;
/**
* Initalise the EventTimer class on page.
* Returns the singleton instance of the EventTimer class and binds
Expand All @@ -32,7 +42,6 @@ export class EventTimer {
* Note: We save to window.guardian.commercialTimer because
* different bundles (DCR / DCP) can use commercial core, and we want
* all timer events saved to a single instance per-page
*
* @returns {EventTimer} Instance of EventTimer
*/
static init(): EventTimer {
Expand All @@ -44,12 +53,12 @@ export class EventTimer {
* Typical use case is EventTimer.get().trigger
*/
static get(): EventTimer {
return EventTimer.init();
return this.init();
}

constructor() {
this.events = [];
this.startTS = performance.now();
this.startTS = window.performance.now();
this.triggers = {
first: {
slotReady: false,
Expand All @@ -66,14 +75,26 @@ export class EventTimer {
adOnPage: false,
},
};
this.gaConfig = {
logEvents: [
{
name: 'slotReady',
label: 'gu.commercial.slotReady',
},
{
name: 'slotInitialised',
label: 'gu.commercial.slotInitialised',
},
],
};
}

mark(name: string): PerformanceEntry {
const longName = `gu.commercial.${name}`;
performance.mark(longName);
window.performance.mark(longName);

// Most recent mark with this name is the event we just created.
const mark = performance
const mark = window.performance
.getEntriesByName(longName, 'mark')
.slice(-1)[0];
this.events.push(new Event(name, mark));
Expand All @@ -92,11 +113,14 @@ export class EventTimer {
const TRACKEDSLOTNAME = 'top-above-nav';
if (origin === 'page') {
this.mark(eventName);
this.trackInGA(eventName, eventName);
return;
}

if (!this.triggers.first[eventName as keyof SlotEventStatus]) {
this.mark(`first-${eventName}`);
const trackLabel = `first-${eventName}`;
this.mark(trackLabel);
this.trackInGA(eventName, trackLabel);
this.triggers.first[eventName as keyof SlotEventStatus] = true;
}

Expand All @@ -106,11 +130,22 @@ export class EventTimer {
eventName as keyof SlotEventStatus
]
) {
this.mark(`${TRACKEDSLOTNAME}-${eventName}`);
const trackLabel = `${TRACKEDSLOTNAME}-${eventName}`;
this.mark(trackLabel);
this.trackInGA(eventName, trackLabel);
this.triggers[TRACKEDSLOTNAME][
eventName as keyof SlotEventStatus
] = true;
}
}
}

trackInGA(eventName: string, label: string): void {
const gaEvent = this.gaConfig.logEvents.find(
(e) => e.name === eventName,
);
if (gaEvent) {
trackEvent(gaEvent.label, label, 'new');
}
}
}
26 changes: 26 additions & 0 deletions src/GoogleAnalytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const trackEvent = (
timingCategory: string,
timingVar: string,
timingLabel: string,
): void => {
const { ga, guardian } = window;
const trackerName: string | undefined =
guardian.config?.googleAnalytics?.trackers.editorial;
if (typeof ga === 'undefined' || typeof trackerName === 'undefined') {
console.error(
"Can't track GA event - GA library not loaded or no tracker found",
);
return;
}
const timeSincePageLoad: number = Math.round(window.performance.now());

const send = `${trackerName}.send`;
window.ga(
send,
'timing',
timingCategory,
timingVar,
timeSincePageLoad,
timingLabel,
);
};
10 changes: 10 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ export type ThirdPartyTag = {
useImage?: boolean;
};

export type GuardianAnalyticsConfig = {
trackers: Record<string, string>;
};

export type GuardianWindowConfig = {
googleAnalytics?: GuardianAnalyticsConfig;
};

export type GoogleTagParams = unknown;
export type GoogleTrackConversionObject = {
google_conversion_id: number;
Expand All @@ -38,6 +46,8 @@ declare global {
googletag?: googletag.Googletag;
guardian: {
commercialTimer?: EventTimer;
config?: GuardianWindowConfig;
};
ga: UniversalAnalytics.ga;
}
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,11 @@
resolved "https://registry.yarnpkg.com/@types/doubleclick-gpt/-/doubleclick-gpt-2019041801.0.4.tgz#6cd7abdb4b38216c5ee46d28b8f8d3054c83b365"
integrity sha512-VZkwxAGKpKKgwf95T2NhB1MLo3uwMX1iN+KnfywCfpYVzjTtQREUoqtWf4pYmsSrub/st889oXE5JU1Qp1v6mQ==

"@types/google.analytics@^0.0.41":
version "0.0.41"
resolved "https://registry.yarnpkg.com/@types/google.analytics/-/google.analytics-0.0.41.tgz#7905553890bb8651c93927c1ce4c1c4a26e41505"
integrity sha512-LE6AfJFJtxUl1d56oQ1PvQDEOtU+0fgqTVgmOw+T0eaz7FB4DTqKxbI6x8sq+7FDvAeIXOugZyGHJLR4CAxO4A==

"@types/graceful-fs@^4.1.2":
version "4.1.5"
resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15"
Expand Down

0 comments on commit f868615

Please sign in to comment.