import { afterEach, describe, expect, it } from "vitest"; import { clear, enabled, errorAttrs, events, log, newRequestId, RING_MAX, setLevel } from "./log"; afterEach(() => { clear(); setLevel("INFO"); }); describe("client log", () => { it("keeps an event in the server's shape", () => { log.warn("nav.view", "page opened", { page: "feed" }); const [e] = events(); expect(e).toMatchObject({ level: "WARNING", sev: 13, source: "web", event: "nav.view", msg: "page opened", attrs: { page: "feed" }, }); expect(e!.tab_id).toMatch(/^[0-9a-f]{12}$/); expect(Number.isNaN(Date.parse(e!.ts))).toBe(false); }); it("drops what is below the level, and the level can be lowered", () => { log.debug("a", "hidden"); expect(events()).toEqual([]); expect(enabled("DEBUG")).toBe(false); setLevel("TRACE"); log.trace("b", "kept"); expect(events().map((e) => e.event)).toEqual(["b"]); }); it("redacts at capture — message and attributes alike", () => { log.info("auth.link", "sent to someone@mail.example", { password: "hunter2", url: "/?reset=abc123", }); const [e] = events(); expect(e!.msg).toBe("sent to [email]"); expect(e!.attrs).toEqual({ password: "[redacted]", url: "/?reset=[redacted]" }); }); it("is a ring: the oldest events go once it is full", () => { for (let i = 0; i < RING_MAX + 5; i++) log.info("n", String(i)); const kept = events(); expect(kept).toHaveLength(RING_MAX); expect(kept[0]!.msg).toBe("5"); }); it("never throws into the caller", () => { const hostile = { get boom() { throw new Error("getter"); }, }; expect(() => log.error("x", "y", { hostile })).not.toThrow(); }); it("makes request ids the server accepts", () => { // logs._REQUEST_ID_RE: [A-Za-z0-9._-]{8,64} const id = newRequestId(); expect(id).toMatch(/^[A-Za-z0-9._-]{8,64}$/); expect(newRequestId()).not.toBe(id); }); it("turns an error into attributes", () => { expect(errorAttrs(new TypeError("bad"))).toMatchObject({ error: "TypeError", message: "bad" }); expect(errorAttrs("plain")).toEqual({ error: "string", message: "plain" }); }); });