From 02aefb3dc197f414044757359f7a68fc6d87bca1 Mon Sep 17 00:00:00 2001 From: DaKheera47 Date: Wed, 25 Feb 2026 21:27:28 +0000 Subject: [PATCH] ids --- orchestrator/src/lib/analytics.test.ts | 19 ++++++++++++ orchestrator/src/lib/analytics.ts | 40 +++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/orchestrator/src/lib/analytics.test.ts b/orchestrator/src/lib/analytics.test.ts index 51adb51..372567b 100644 --- a/orchestrator/src/lib/analytics.test.ts +++ b/orchestrator/src/lib/analytics.test.ts @@ -1,6 +1,7 @@ import { __resetAnalyticsTestState, bucketQueryLength, + trackEvent, trackProductEvent, } from "./analytics"; @@ -12,6 +13,7 @@ describe("analytics", () => { vi.setSystemTime(new Date("2026-02-25T12:00:00Z")); track.mockReset(); __resetAnalyticsTestState(); + window.localStorage.clear(); Object.defineProperty(window, "umami", { configurable: true, value: { track }, @@ -34,6 +36,22 @@ describe("analytics", () => { expect(track).toHaveBeenCalledTimes(2); }); + it("attaches a stable anonymous analytics user id to every event", () => { + trackEvent("star_repo_click", { location: "demo_mode_banner" }); + trackProductEvent("tracer_drilldown_mode_changed", { mode: "all" }); + + expect(track).toHaveBeenCalledTimes(2); + + const firstPayload = track.mock.calls[0][1] as Record; + const secondPayload = track.mock.calls[1][1] as Record; + const storedId = window.localStorage.getItem("jobops.analytics.user_id.v1"); + + expect(typeof firstPayload.analytics_user_id).toBe("string"); + expect(firstPayload.analytics_user_id).toBeTruthy(); + expect(secondPayload.analytics_user_id).toBe(firstPayload.analytics_user_id); + expect(storedId).toBe(firstPayload.analytics_user_id); + }); + it("drops disallowed keys and non-primitive payload values", () => { trackProductEvent("jobs_pipeline_run_started", { mode: "automatic", @@ -57,6 +75,7 @@ describe("analytics", () => { country: "uk", has_city_locations: true, search_terms_count: 3, + analytics_user_id: expect.any(String), }); }); diff --git a/orchestrator/src/lib/analytics.ts b/orchestrator/src/lib/analytics.ts index 3f2f819..0c0627f 100644 --- a/orchestrator/src/lib/analytics.ts +++ b/orchestrator/src/lib/analytics.ts @@ -10,7 +10,15 @@ declare global { export function trackEvent(event: string, data?: Record) { if (typeof window === "undefined") return; - window.umami?.track(event, data); + const analyticsUserId = getAnalyticsUserId(); + const payload = + analyticsUserId === null + ? data + : { + ...(data ?? {}), + analytics_user_id: analyticsUserId, + }; + window.umami?.track(event, payload); } type ProductEventMap = { @@ -114,8 +122,37 @@ type ProductEventName = keyof ProductEventMap; type Primitive = string | number | boolean | null; type SanitizedPayload = Record; +function generateAnalyticsUserId() { + if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { + return crypto.randomUUID(); + } + return `anon_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`; +} + +function getAnalyticsUserId(): string | null { + if (typeof window === "undefined") return null; + if (cachedAnalyticsUserId) return cachedAnalyticsUserId; + + try { + const existing = window.localStorage.getItem(ANALYTICS_USER_ID_STORAGE_KEY); + if (existing) { + cachedAnalyticsUserId = existing; + return existing; + } + + const next = generateAnalyticsUserId(); + window.localStorage.setItem(ANALYTICS_USER_ID_STORAGE_KEY, next); + cachedAnalyticsUserId = next; + return next; + } catch { + return null; + } +} + const DEDUPE_WINDOW_MS = 3_000; +const ANALYTICS_USER_ID_STORAGE_KEY = "jobops.analytics.user_id.v1"; const recentEventCache = new Map(); +let cachedAnalyticsUserId: string | null = null; const DISALLOWED_KEY_PARTS = [ "query", "url", @@ -221,4 +258,5 @@ export function bucketQueryLength(value: string | number): string { export function __resetAnalyticsTestState() { recentEventCache.clear(); + cachedAnalyticsUserId = null; }