This commit is contained in:
DaKheera47 2026-02-25 21:27:28 +00:00
parent cbc52cbac0
commit 02aefb3dc1
2 changed files with 58 additions and 1 deletions

View File

@ -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<string, unknown>;
const secondPayload = track.mock.calls[1][1] as Record<string, unknown>;
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),
});
});

View File

@ -10,7 +10,15 @@ declare global {
export function trackEvent(event: string, data?: Record<string, unknown>) {
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<string, Primitive>;
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<string, number>();
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;
}