Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
type Callback = (data?: EventData) => void;
type EventData = unknown;

export type Event = "beacon" | "new_page_id";

const subscribers: Partial<Record<Event, Callback[]>> = {};
const eventData: Partial<Record<Event, EventData>> = {};

export function subscribe(event: Event, callback: Callback): void {
if (!subscribers[event]) {
subscribers[event] = [];
}

subscribers[event].push(callback);

// Ensure previous event data is available to new subscribers
if (eventData[event] !== undefined) {
callback(eventData[event]);
}
}

export function emit(event: Event, data?: EventData): void {
eventData[event] = data;

if (!subscribers[event]) {
return;
}

subscribers[event].forEach((callback) => callback(data));
}
8 changes: 5 additions & 3 deletions src/global.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { UserConfig } from "./config";
import { LogEventRecord } from "./logger";
import type { UserConfig } from "./config";
import type { Event } from "./events";
import type { LogEventRecord } from "./logger";

export type Command = [CommandFunction, ...CommandArg[]];
type CommandFunction = "addData" | "init" | "mark" | "markLoadTime" | "measure" | "send";
type CommandFunction = "addData" | "init" | "mark" | "markLoadTime" | "measure" | "on" | "send";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type CommandArg = any;
type PerfMarkFn = typeof performance.mark;
Expand All @@ -22,6 +23,7 @@ export interface LuxGlobal extends UserConfig {
mark: (...args: Parameters<PerfMarkFn>) => ReturnType<PerfMarkFn> | void;
markLoadTime?: (time?: number) => void;
measure: (...args: Parameters<PerfMeasureFn>) => ReturnType<PerfMeasureFn> | void;
on: (event: Event, callback: (data?: unknown) => void) => void;
/** Timestamp representing when the LUX snippet was evaluated */
ns?: number;
send: () => void;
Expand Down
14 changes: 12 additions & 2 deletions src/lux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { SESSION_COOKIE_NAME } from "./cookie";
import * as CustomData from "./custom-data";
import { onVisible, isVisible, wasPrerendered, wasRedirected } from "./document";
import { getNodeSelector } from "./dom";
import * as Events from "./events";
import Flags, { addFlag } from "./flags";
import { Command, LuxGlobal } from "./global";
import { getTrackingParams } from "./integrations/tracking";
Expand Down Expand Up @@ -1689,6 +1690,8 @@ LUX = (function () {

function _sendBeacon(url: string) {
new Image().src = url;

Events.emit("beacon", url);
}

// INTERACTION METRICS
Expand Down Expand Up @@ -1826,12 +1829,18 @@ LUX = (function () {
// (because they get sent at different times). Each "page view" (including SPA) should have a
// unique gSyncId.
function createSyncId(inSampleBucket = false): string {
let syncId: string;

if (inSampleBucket) {
// "00" matches all sample rates
return Number(new Date()) + "00000";
syncId = Number(new Date()) + "00000";
} else {
syncId = Number(new Date()) + padStart(String(round(100000 * Math.random())), 5, "0");
}

return Number(new Date()) + padStart(String(round(100000 * Math.random())), 5, "0");
Events.emit("new_page_id", syncId);

return syncId;
}

// Unique ID (also known as Session ID)
Expand Down Expand Up @@ -2031,6 +2040,7 @@ LUX = (function () {
globalLux.measure = _measure;
globalLux.init = _init;
globalLux.markLoadTime = _markLoadTime;
globalLux.on = Events.subscribe;
globalLux.send = () => {
logger.logEvent(LogEvent.SendCalled);
beacon.send();
Expand Down
3 changes: 2 additions & 1 deletion src/snippet.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Command, LuxGlobal } from "./global";
import type { Command, LuxGlobal } from "./global";
import { performance } from "./performance";
import scriptStartTime from "./start-marker";
import { msSinceNavigationStart } from "./timing";
Expand All @@ -20,6 +20,7 @@ LUX.init = () => LUX.cmd(["init"]);
LUX.mark = _mark;
LUX.markLoadTime = () => LUX.cmd(["markLoadTime", msSinceNavigationStart()]);
LUX.measure = _measure;
LUX.on = (event, callback) => LUX.cmd(["on", event, callback]);
LUX.send = () => LUX.cmd(["send"]);
LUX.ns = scriptStartTime;

Expand Down
48 changes: 48 additions & 0 deletions tests/integration/events.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { test, expect } from "@playwright/test";
import { getSearchParam } from "../helpers/lux";
import RequestInterceptor from "../request-interceptor";

test.describe("LUX events", () => {
test("new_page_id", async ({ page }) => {
const luxRequests = new RequestInterceptor(page).createRequestMatcher("/beacon/");
await page.goto(
"/default.html?injectScript=LUX.auto=false;LUX.on('new_page_id', (id) => window.page_id = id);",
{ waitUntil: "networkidle" },
);
await luxRequests.waitForMatchingRequest(() => page.evaluate(() => LUX.send()));

const firstPageId = await page.evaluate(() => window.page_id);
const firstBeacon = luxRequests.getUrl(0)!;
expect(firstPageId).toEqual(getSearchParam(firstBeacon, "sid"));

await luxRequests.waitForMatchingRequest(() =>
page.evaluate(() => {
LUX.init();
LUX.send();
}),
);

const secondBeacon = luxRequests.getUrl(1)!;
const secondPageId = await page.evaluate(() => window.page_id);
expect(secondPageId).toEqual(getSearchParam(secondBeacon, "sid"));
});

test("beacon", async ({ page }) => {
const luxRequests = new RequestInterceptor(page).createRequestMatcher("/beacon/");
await page.goto(
"/default.html?injectScript=LUX.auto=false;LUX.on('beacon', (url) => window.beacon_url = url);",
{ waitUntil: "networkidle" },
);
await luxRequests.waitForMatchingRequest(() => page.evaluate(() => LUX.send()));

const beacon = luxRequests.getUrl(0)!;
let beaconUrl = await page.evaluate(() => window.beacon_url);

// We don't encode the Delivery Type parameter before sending the beacon, but Chromium seems to
// check that everything is encoded before making the actual request. This is a small hack to
// allow us to compare the strings.
beaconUrl = beaconUrl.replace("dt(empty string)_", "dt(empty%20string)_");

expect(beaconUrl).toEqual(beacon.href);
});
});
2 changes: 1 addition & 1 deletion tests/integration/unload.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { test, expect } from "@playwright/test";
import Flags from "../../src/flags";
import { setPageHidden } from "../helpers/browsers";
import { hasFlag } from "../helpers/lux";
import RequestInterceptor from "../request-interceptor";
import { setPageHidden } from "../helpers/browsers";

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