lib/analytics-sanitize.test.ts
Rama main · réplica de lectura del repositorio original.
import { describe, expect, it } from "vitest";
import {
durationBucket,
lengthBucket,
sanitizeAnalyticsEventName,
sanitizeAnalyticsProps,
sizeBucket,
} from "./analytics-sanitize";
describe("sanitizeAnalyticsEventName", () => {
it("laisse passer un nom snake_case du catalogue", () => {
expect(sanitizeAnalyticsEventName("issue_created")).toBe("issue_created");
expect(sanitizeAnalyticsEventName("mcp:tool-called")).toBe("mcp:tool-called");
});
it("normalise la casse", () => {
expect(sanitizeAnalyticsEventName("Issue_Created")).toBe("issue_created");
});
it("rejette tout ce qui n'est pas un nom exploitable", () => {
// An empty string creates a spurious event definition in PostHog.
expect(sanitizeAnalyticsEventName("")).toBe("");
expect(sanitizeAnalyticsEventName("issue created")).toBe("");
expect(sanitizeAnalyticsEventName("issue.created")).toBe("");
expect(sanitizeAnalyticsEventName("$pageview")).toBe("");
expect(sanitizeAnalyticsEventName(42)).toBe("");
expect(sanitizeAnalyticsEventName(null)).toBe("");
expect(sanitizeAnalyticsEventName(undefined)).toBe("");
});
it("borne la longueur", () => {
expect(sanitizeAnalyticsEventName("a".repeat(200))).toHaveLength(64);
});
});
describe("sanitizeAnalyticsProps", () => {
it("garde les primitives", () => {
expect(
sanitizeAnalyticsProps({ count: 3, ok: true, status: "todo", missing: null })
).toEqual({ count: 3, ok: true, status: "todo", missing: null });
});
it("discards non-serializable values", () => {
expect(
sanitizeAnalyticsProps({
nested: { a: 1 },
list: [1, 2],
fn: () => null,
nope: undefined,
nan: Number.NaN,
inf: Number.POSITIVE_INFINITY,
})
).toEqual({});
});
it("discards reserved PostHog keys and malformed keys", () => {
expect(sanitizeAnalyticsProps({ $set: "x", "bad key": 1, "a.b": 2 })).toEqual({});
});
it("limits text to 512 characters", () => {
const out = sanitizeAnalyticsProps({ reason: "x".repeat(1000) });
expect(String(out.reason)).toHaveLength(512);
});
it("neutralizes control characters", () => {
expect(sanitizeAnalyticsProps({ reason: "a\nb\tc" })).toEqual({ reason: "a b c" });
});
it("caps the number of keys", () => {
const many = Object.fromEntries(
Array.from({ length: 40 }, (_, i) => [`k${i}`, i])
);
expect(Object.keys(sanitizeAnalyticsProps(many))).toHaveLength(24);
});
it("tolerates missing props", () => {
expect(sanitizeAnalyticsProps(undefined)).toEqual({});
});
});
describe("tranches", () => {
it("lengthBucket measures without revealing the content", () => {
expect(lengthBucket("")).toBe("empty");
expect(lengthBucket(null)).toBe("empty");
expect(lengthBucket("court")).toBe("xs");
expect(lengthBucket("x".repeat(100))).toBe("s");
expect(lengthBucket("x".repeat(300))).toBe("m");
expect(lengthBucket("x".repeat(1000))).toBe("l");
expect(lengthBucket("x".repeat(5000))).toBe("xl");
});
it("durationBucket", () => {
expect(durationBucket(500)).toBe("under_1s");
expect(durationBucket(3_000)).toBe("1_5s");
expect(durationBucket(60_000)).toBe("30s_2m");
expect(durationBucket(3_600_000)).toBe("over_10m");
expect(durationBucket(Number.NaN)).toBe("unknown");
});
it("sizeBucket", () => {
expect(sizeBucket(1_000)).toBe("under_100kb");
expect(sizeBucket(500_000)).toBe("100kb_1mb");
expect(sizeBucket(9_000_000)).toBe("over_5mb");
expect(sizeBucket(-1)).toBe("unknown");
});
});