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
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// @vitest-environment jsdom

import { Result } from "@stackframe/stack-shared/dist/utils/results";
import { afterEach, describe, expect, it, vi } from "vitest";
import { EventTracker } from "./event-tracker";

async function advancePastAccessTokenRefresh() {
await vi.advanceTimersByTimeAsync(10_000);
await Promise.resolve();
await vi.advanceTimersByTimeAsync(10_000);
await Promise.resolve();
}

function getSentEventTypes(sentBodies: string[]) {
const [body] = sentBodies;

const payload = JSON.parse(body);
if (typeof payload !== "object" || payload === null || !("events" in payload) || !Array.isArray(payload.events)) {
throw new Error("Expected analytics batch payload to include an events array.");
}

return (payload.events as { event_type: string }[]).map((event) => event.event_type);
}
Comment thread
BilalG1 marked this conversation as resolved.

describe("EventTracker", () => {
afterEach(() => {
vi.useRealTimers();
});

it("captures events when browser globals are exposed as accessor descriptors", async () => {
vi.useFakeTimers();
document.body.innerHTML = "<button>Open project</button>";

const screenDescriptor = Object.getOwnPropertyDescriptor(window, "screen");
const historyDescriptor = Object.getOwnPropertyDescriptor(window, "history");
expect(screenDescriptor?.value).toBeUndefined();
expect(historyDescriptor?.value).toBeUndefined();
expect(screenDescriptor?.get).toBeTypeOf("function");
expect(historyDescriptor?.get).toBeTypeOf("function");

const sentBodies: string[] = [];
const tracker = new EventTracker({
projectId: "internal",
getAccessToken: async () => "access-token",
sendBatch: async (body) => {
sentBodies.push(body);
return Result.ok(new Response());
},
});

try {
tracker.start();
document.querySelector("button")?.dispatchEvent(new MouseEvent("click", {
bubbles: true,
clientX: 12,
clientY: 34,
}));

await advancePastAccessTokenRefresh();

expect(getSentEventTypes(sentBodies)).toMatchInlineSnapshot(`
[
"$page-view",
"$click",
]
`);
} finally {
tracker.stop();
}
});

it("captures client-side navigations when history is exposed as an accessor descriptor", async () => {
vi.useFakeTimers();

const historyDescriptor = Object.getOwnPropertyDescriptor(window, "history");
expect(historyDescriptor?.value).toBeUndefined();
expect(historyDescriptor?.get).toBeTypeOf("function");

const sentBodies: string[] = [];
const tracker = new EventTracker({
projectId: "internal",
getAccessToken: async () => "access-token",
sendBatch: async (body) => {
sentBodies.push(body);
return Result.ok(new Response());
},
});

try {
tracker.start();
window.history.pushState({}, "", "/projects/test-project");

await advancePastAccessTokenRefresh();

expect(getSentEventTypes(sentBodies)).toMatchInlineSnapshot(`
[
"$page-view",
"$page-view",
]
`);
} finally {
tracker.stop();
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,12 @@ export class EventTracker {
start() {
if (this._started) return;
if (!isBrowserLike()) return;
const screenObject = Object.getOwnPropertyDescriptor(window, "screen")?.value;
if (
typeof window.addEventListener !== "function"
|| typeof window.removeEventListener !== "function"
|| typeof document.addEventListener !== "function"
|| typeof document.removeEventListener !== "function"
|| !hasScreenDimensions(screenObject)
|| !hasScreenDimensions(window.screen)
) {
return;
}
Expand Down Expand Up @@ -105,7 +104,7 @@ export class EventTracker {
}

private _capturePageView(entryType: "initial" | "push" | "replace" | "pop") {
const screenObject = Object.getOwnPropertyDescriptor(window, "screen")?.value;
const screenObject = window.screen;
if (!hasScreenDimensions(screenObject)) {
return;
}
Expand Down Expand Up @@ -134,7 +133,7 @@ export class EventTracker {
private _setupPageViewCapture() {
// Fire initial page-view
this._capturePageView("initial");
const historyObject = Object.getOwnPropertyDescriptor(window, "history")?.value;
const historyObject = window.history;
if (!hasHistoryMethods(historyObject)) {
return;
}
Expand Down Expand Up @@ -246,7 +245,7 @@ export class EventTracker {
}

// Restore history methods
const historyObject = Object.getOwnPropertyDescriptor(window, "history")?.value;
const historyObject = window.history;
if (hasHistoryMethods(historyObject)) {
if (this._originalPushState) {
historyObject.pushState = this._originalPushState;
Expand Down
Loading