/** * Unit tests for the analytics tracker. */ import { describe, expect, it, vi } from "vitest"; import { createTracker, appFirstOpen, scanStarted } from "@app/analytics"; import type { AnalyticsBatchResult, AnalyticsEvent, SendBatch } from "@app/analytics"; describe("createTracker", () => { it("enriches events with default context and flushes immediately when batch is full", async () => { const sent: AnalyticsEvent[][] = []; const send: SendBatch = vi.fn(async (events) => { sent.push(events); return { accepted: events.length } satisfies AnalyticsBatchResult; }); const tracker = createTracker( { apiBaseUrl: "http://localhost:4000", anonymousId: "anon-1", sessionId: "session-1", appVersion: "1.0.0", platform: "ios", locale: "sv-SE", maxBatchSize: 2, flushIntervalMs: 0, }, send, ); tracker.track(appFirstOpen()); tracker.track(scanStarted({ properties: { source: "camera" } })); await tracker.flush(); expect(sent).toHaveLength(1); const firstBatch = sent[0]!; expect(firstBatch).toHaveLength(2); expect(firstBatch[0]).toBeDefined(); expect(firstBatch[0]!.name).toBe("app_first_open"); expect(firstBatch[0]!.anonymousId).toBe("anon-1"); expect(firstBatch[0]!.sessionId).toBe("session-1"); expect(firstBatch[0]!.appVersion).toBe("1.0.0"); expect(firstBatch[0]!.platform).toBe("ios"); expect(firstBatch[0]!.locale).toBe("sv-SE"); expect(firstBatch[0]!.occurredAt).toBeDefined(); expect(firstBatch[1]).toBeDefined(); expect(firstBatch[1]!.name).toBe("scan_started"); expect(firstBatch[1]!.properties).toEqual({ source: "camera" }); tracker.destroy(); }); it("requeues events when send fails", async () => { let calls = 0; const send: SendBatch = vi.fn(async () => { calls++; if (calls === 1) throw new Error("network"); return { accepted: 1 }; }); const tracker = createTracker( { apiBaseUrl: "http://localhost:4000", anonymousId: "anon-2", appVersion: "1.0.0", platform: "android", locale: "en-US", maxBatchSize: 1, flushIntervalMs: 0, }, send, ); tracker.track(appFirstOpen()); const first = await tracker.flush(); expect(first.accepted).toBe(0); const second = await tracker.flush(); expect(second.accepted).toBe(1); expect(send).toHaveBeenCalledTimes(2); tracker.destroy(); }); it("drops validation-rejected events instead of retrying forever", async () => { const send: SendBatch = vi.fn(async () => ({ accepted: 0, rejected: 1, errors: [{ index: 0, message: "validation error, do not retry" }], })); const tracker = createTracker( { apiBaseUrl: "http://localhost:4000", anonymousId: "anon-3", appVersion: "1.0.0", platform: "web", locale: "sv-SE", maxBatchSize: 100, flushIntervalMs: 0, }, send, ); tracker.track(appFirstOpen()); const result = await tracker.flush(); expect(result.rejected).toBe(1); // Queue should be empty; second flush returns zero. const second = await tracker.flush(); expect(second.accepted).toBe(0); expect(send).toHaveBeenCalledTimes(1); tracker.destroy(); }); });