ids
This commit is contained in:
parent
cbc52cbac0
commit
02aefb3dc1
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
__resetAnalyticsTestState,
|
__resetAnalyticsTestState,
|
||||||
bucketQueryLength,
|
bucketQueryLength,
|
||||||
|
trackEvent,
|
||||||
trackProductEvent,
|
trackProductEvent,
|
||||||
} from "./analytics";
|
} from "./analytics";
|
||||||
|
|
||||||
@ -12,6 +13,7 @@ describe("analytics", () => {
|
|||||||
vi.setSystemTime(new Date("2026-02-25T12:00:00Z"));
|
vi.setSystemTime(new Date("2026-02-25T12:00:00Z"));
|
||||||
track.mockReset();
|
track.mockReset();
|
||||||
__resetAnalyticsTestState();
|
__resetAnalyticsTestState();
|
||||||
|
window.localStorage.clear();
|
||||||
Object.defineProperty(window, "umami", {
|
Object.defineProperty(window, "umami", {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: { track },
|
value: { track },
|
||||||
@ -34,6 +36,22 @@ describe("analytics", () => {
|
|||||||
expect(track).toHaveBeenCalledTimes(2);
|
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", () => {
|
it("drops disallowed keys and non-primitive payload values", () => {
|
||||||
trackProductEvent("jobs_pipeline_run_started", {
|
trackProductEvent("jobs_pipeline_run_started", {
|
||||||
mode: "automatic",
|
mode: "automatic",
|
||||||
@ -57,6 +75,7 @@ describe("analytics", () => {
|
|||||||
country: "uk",
|
country: "uk",
|
||||||
has_city_locations: true,
|
has_city_locations: true,
|
||||||
search_terms_count: 3,
|
search_terms_count: 3,
|
||||||
|
analytics_user_id: expect.any(String),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,15 @@ declare global {
|
|||||||
|
|
||||||
export function trackEvent(event: string, data?: Record<string, unknown>) {
|
export function trackEvent(event: string, data?: Record<string, unknown>) {
|
||||||
if (typeof window === "undefined") return;
|
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 = {
|
type ProductEventMap = {
|
||||||
@ -114,8 +122,37 @@ type ProductEventName = keyof ProductEventMap;
|
|||||||
type Primitive = string | number | boolean | null;
|
type Primitive = string | number | boolean | null;
|
||||||
type SanitizedPayload = Record<string, Primitive>;
|
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 DEDUPE_WINDOW_MS = 3_000;
|
||||||
|
const ANALYTICS_USER_ID_STORAGE_KEY = "jobops.analytics.user_id.v1";
|
||||||
const recentEventCache = new Map<string, number>();
|
const recentEventCache = new Map<string, number>();
|
||||||
|
let cachedAnalyticsUserId: string | null = null;
|
||||||
const DISALLOWED_KEY_PARTS = [
|
const DISALLOWED_KEY_PARTS = [
|
||||||
"query",
|
"query",
|
||||||
"url",
|
"url",
|
||||||
@ -221,4 +258,5 @@ export function bucketQueryLength(value: string | number): string {
|
|||||||
|
|
||||||
export function __resetAnalyticsTestState() {
|
export function __resetAnalyticsTestState() {
|
||||||
recentEventCache.clear();
|
recentEventCache.clear();
|
||||||
|
cachedAnalyticsUserId = null;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user