From a0fca107d67a977fb52a1dc2b70ed0db360f5e63 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Sun, 12 Apr 2026 21:52:33 -0700 Subject: [PATCH 1/2] Update pre-push.md --- .cursor/commands/pre-push.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cursor/commands/pre-push.md b/.cursor/commands/pre-push.md index 5c2eb56a0..103f94c10 100644 --- a/.cursor/commands/pre-push.md +++ b/.cursor/commands/pre-push.md @@ -1 +1 @@ -Please compare `dev` to `main` and ensure that all migrations are backwards compatible. In what ways could breakage occur? Report the result to me in detail. +Please compare `dev` to `main` and ensure that all migrations are backwards compatible. In what ways could breakage occur? Report the result to me in detail. Anything else that's scary that could occur, or that we should think about while migrating? From 310278781a83155dd7e85e3bd6dd92de14e99ff6 Mon Sep 17 00:00:00 2001 From: BilalG1 Date: Mon, 13 Apr 2026 09:24:40 -0700 Subject: [PATCH 2/2] Fix EventTracker silently dormant in real browsers (#1327) `window.screen` and `window.history` are accessor properties on `Window.prototype`, so `Object.getOwnPropertyDescriptor(window, X)?.value` returned undefined in real browsers, causing `start()` to short-circuit and never capture or send any $page-view / $click events. Read the globals directly instead; the jsdom-based regression test pins the accessor-descriptor shape so this can't silently come back. ## Summary by CodeRabbit * **Tests** * Added a new test suite verifying event batching, timing, page-view and click event capture, and client-side navigation behavior using simulated timers and DOM environment. * **Bug Fixes** * Improved event tracker reliability by changing how browser screen and history are read, yielding more consistent detection of screen dimensions and navigation for analytics capture. --- .../implementations/event-tracker.test.ts | 105 ++++++++++++++++++ .../apps/implementations/event-tracker.ts | 9 +- 2 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 packages/template/src/lib/stack-app/apps/implementations/event-tracker.test.ts diff --git a/packages/template/src/lib/stack-app/apps/implementations/event-tracker.test.ts b/packages/template/src/lib/stack-app/apps/implementations/event-tracker.test.ts new file mode 100644 index 000000000..a69dee67c --- /dev/null +++ b/packages/template/src/lib/stack-app/apps/implementations/event-tracker.test.ts @@ -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); +} + +describe("EventTracker", () => { + afterEach(() => { + vi.useRealTimers(); + }); + + it("captures events when browser globals are exposed as accessor descriptors", async () => { + vi.useFakeTimers(); + document.body.innerHTML = ""; + + 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(); + } + }); +}); diff --git a/packages/template/src/lib/stack-app/apps/implementations/event-tracker.ts b/packages/template/src/lib/stack-app/apps/implementations/event-tracker.ts index 04cbe403f..c46a652d2 100644 --- a/packages/template/src/lib/stack-app/apps/implementations/event-tracker.ts +++ b/packages/template/src/lib/stack-app/apps/implementations/event-tracker.ts @@ -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; } @@ -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; } @@ -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; } @@ -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;