Skip to content

Commit 4860d1f

Browse files
Merge pull request #73 from SpeedCurve-Metrics/feature/events
Add `LUX.on()` to subscribe to events
2 parents 214e510 + decd05c commit 4860d1f

File tree

6 files changed

+98
-7
lines changed

6 files changed

+98
-7
lines changed

src/events.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
type Callback = (data?: EventData) => void;
2+
type EventData = unknown;
3+
4+
export type Event = "beacon" | "new_page_id";
5+
6+
const subscribers: Partial<Record<Event, Callback[]>> = {};
7+
const eventData: Partial<Record<Event, EventData>> = {};
8+
9+
export function subscribe(event: Event, callback: Callback): void {
10+
if (!subscribers[event]) {
11+
subscribers[event] = [];
12+
}
13+
14+
subscribers[event].push(callback);
15+
16+
// Ensure previous event data is available to new subscribers
17+
if (eventData[event] !== undefined) {
18+
callback(eventData[event]);
19+
}
20+
}
21+
22+
export function emit(event: Event, data?: EventData): void {
23+
eventData[event] = data;
24+
25+
if (!subscribers[event]) {
26+
return;
27+
}
28+
29+
subscribers[event].forEach((callback) => callback(data));
30+
}

src/global.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { UserConfig } from "./config";
2-
import { LogEventRecord } from "./logger";
1+
import type { UserConfig } from "./config";
2+
import type { Event } from "./events";
3+
import type { LogEventRecord } from "./logger";
34

45
export type Command = [CommandFunction, ...CommandArg[]];
5-
type CommandFunction = "addData" | "init" | "mark" | "markLoadTime" | "measure" | "send";
6+
type CommandFunction = "addData" | "init" | "mark" | "markLoadTime" | "measure" | "on" | "send";
67
// eslint-disable-next-line @typescript-eslint/no-explicit-any
78
type CommandArg = any;
89
type PerfMarkFn = typeof performance.mark;
@@ -22,6 +23,7 @@ export interface LuxGlobal extends UserConfig {
2223
mark: (...args: Parameters<PerfMarkFn>) => ReturnType<PerfMarkFn> | void;
2324
markLoadTime?: (time?: number) => void;
2425
measure: (...args: Parameters<PerfMeasureFn>) => ReturnType<PerfMeasureFn> | void;
26+
on: (event: Event, callback: (data?: unknown) => void) => void;
2527
/** Timestamp representing when the LUX snippet was evaluated */
2628
ns?: number;
2729
send: () => void;

src/lux.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { SESSION_COOKIE_NAME } from "./cookie";
1212
import * as CustomData from "./custom-data";
1313
import { onVisible, isVisible, wasPrerendered, wasRedirected } from "./document";
1414
import { getNodeSelector } from "./dom";
15+
import * as Events from "./events";
1516
import Flags, { addFlag } from "./flags";
1617
import { Command, LuxGlobal } from "./global";
1718
import { getTrackingParams } from "./integrations/tracking";
@@ -1689,6 +1690,8 @@ LUX = (function () {
16891690

16901691
function _sendBeacon(url: string) {
16911692
new Image().src = url;
1693+
1694+
Events.emit("beacon", url);
16921695
}
16931696

16941697
// INTERACTION METRICS
@@ -1826,12 +1829,18 @@ LUX = (function () {
18261829
// (because they get sent at different times). Each "page view" (including SPA) should have a
18271830
// unique gSyncId.
18281831
function createSyncId(inSampleBucket = false): string {
1832+
let syncId: string;
1833+
18291834
if (inSampleBucket) {
18301835
// "00" matches all sample rates
1831-
return Number(new Date()) + "00000";
1836+
syncId = Number(new Date()) + "00000";
1837+
} else {
1838+
syncId = Number(new Date()) + padStart(String(round(100000 * Math.random())), 5, "0");
18321839
}
18331840

1834-
return Number(new Date()) + padStart(String(round(100000 * Math.random())), 5, "0");
1841+
Events.emit("new_page_id", syncId);
1842+
1843+
return syncId;
18351844
}
18361845

18371846
// Unique ID (also known as Session ID)
@@ -2031,6 +2040,7 @@ LUX = (function () {
20312040
globalLux.measure = _measure;
20322041
globalLux.init = _init;
20332042
globalLux.markLoadTime = _markLoadTime;
2043+
globalLux.on = Events.subscribe;
20342044
globalLux.send = () => {
20352045
logger.logEvent(LogEvent.SendCalled);
20362046
beacon.send();

src/snippet.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Command, LuxGlobal } from "./global";
1+
import type { Command, LuxGlobal } from "./global";
22
import { performance } from "./performance";
33
import scriptStartTime from "./start-marker";
44
import { msSinceNavigationStart } from "./timing";
@@ -20,6 +20,7 @@ LUX.init = () => LUX.cmd(["init"]);
2020
LUX.mark = _mark;
2121
LUX.markLoadTime = () => LUX.cmd(["markLoadTime", msSinceNavigationStart()]);
2222
LUX.measure = _measure;
23+
LUX.on = (event, callback) => LUX.cmd(["on", event, callback]);
2324
LUX.send = () => LUX.cmd(["send"]);
2425
LUX.ns = scriptStartTime;
2526

tests/integration/events.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { test, expect } from "@playwright/test";
2+
import { getSearchParam } from "../helpers/lux";
3+
import RequestInterceptor from "../request-interceptor";
4+
5+
test.describe("LUX events", () => {
6+
test("new_page_id", async ({ page }) => {
7+
const luxRequests = new RequestInterceptor(page).createRequestMatcher("/beacon/");
8+
await page.goto(
9+
"/default.html?injectScript=LUX.auto=false;LUX.on('new_page_id', (id) => window.page_id = id);",
10+
{ waitUntil: "networkidle" },
11+
);
12+
await luxRequests.waitForMatchingRequest(() => page.evaluate(() => LUX.send()));
13+
14+
const firstPageId = await page.evaluate(() => window.page_id);
15+
const firstBeacon = luxRequests.getUrl(0)!;
16+
expect(firstPageId).toEqual(getSearchParam(firstBeacon, "sid"));
17+
18+
await luxRequests.waitForMatchingRequest(() =>
19+
page.evaluate(() => {
20+
LUX.init();
21+
LUX.send();
22+
}),
23+
);
24+
25+
const secondBeacon = luxRequests.getUrl(1)!;
26+
const secondPageId = await page.evaluate(() => window.page_id);
27+
expect(secondPageId).toEqual(getSearchParam(secondBeacon, "sid"));
28+
});
29+
30+
test("beacon", async ({ page }) => {
31+
const luxRequests = new RequestInterceptor(page).createRequestMatcher("/beacon/");
32+
await page.goto(
33+
"/default.html?injectScript=LUX.auto=false;LUX.on('beacon', (url) => window.beacon_url = url);",
34+
{ waitUntil: "networkidle" },
35+
);
36+
await luxRequests.waitForMatchingRequest(() => page.evaluate(() => LUX.send()));
37+
38+
const beacon = luxRequests.getUrl(0)!;
39+
let beaconUrl = await page.evaluate(() => window.beacon_url);
40+
41+
// We don't encode the Delivery Type parameter before sending the beacon, but Chromium seems to
42+
// check that everything is encoded before making the actual request. This is a small hack to
43+
// allow us to compare the strings.
44+
beaconUrl = beaconUrl.replace("dt(empty string)_", "dt(empty%20string)_");
45+
46+
expect(beaconUrl).toEqual(beacon.href);
47+
});
48+
});

tests/integration/unload.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { test, expect } from "@playwright/test";
22
import Flags from "../../src/flags";
3+
import { setPageHidden } from "../helpers/browsers";
34
import { hasFlag } from "../helpers/lux";
45
import RequestInterceptor from "../request-interceptor";
5-
import { setPageHidden } from "../helpers/browsers";
66

77
test.describe("LUX unload behaviour", () => {
88
test("not automatically sending a beacon when the user navigates away from a page with LUX.auto = false", async ({

0 commit comments

Comments
 (0)