Files
Cibello-app/packages/analytics/test/tracker.test.ts
T

116 lines
3.3 KiB
TypeScript

/**
* 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();
});
});