import { describe, expect, it, vi } from "vitest"; import { createLogger, type LogEvent } from "../src/index"; import { createSentrySink, toSentryLogPayload, toSentrySeverity } from "../src/sentry"; function createFakeSentry() { const scope = { setLevel: vi.fn(), setTag: vi.fn(), setExtras: vi.fn() }; return { scope, sentry: { withScope: (callback: (value: typeof scope) => void) => callback(scope), captureException: vi.fn(), captureMessage: vi.fn(), logger: { trace: vi.fn(), debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), }, }, }; } function issueEvent(overrides: Partial = {}): LogEvent { return { level: "error", arguments: ["message"], timestamp: new Date(), environment: "production", sendToSentryLogs: false, sendToSentryIssue: true, sendToConsole: false, ...overrides, }; } describe("toSentrySeverity", () => { it("maps logger levels to Sentry Issue severities", () => { expect(toSentrySeverity("warn")).toBe("warning"); expect(toSentrySeverity("trace")).toBe("debug"); expect(toSentrySeverity("error")).toBe("error"); expect(toSentrySeverity("info")).toBe("info"); expect(toSentrySeverity("debug")).toBe("debug"); }); }); describe("toSentryLogPayload", () => { it("joins string messages and flattens primitive object attributes", () => { expect( toSentryLogPayload({ level: "info", namespace: "API", arguments: ["Cache warmed", { entries: 42, ok: true, nested: { a: 1 } }], timestamp: new Date(), environment: "production", sendToSentryLogs: true, sendToSentryIssue: false, sendToConsole: false, }), ).toEqual({ message: "Cache warmed", attributes: { "logger.namespace": "API", entries: 42, ok: true, }, }); }); }); describe("createSentrySink", () => { it("captures errors as Issues with namespace and structured attributes", () => { const { scope, sentry } = createFakeSentry(); const logger = createLogger({ environment: "production", namespace: "API", sinks: [createSentrySink(sentry)], }); const error = new Error("offline"); logger.warn("ignored without logs"); logger.error("Request failed", error, { requestId: "req_1" }); expect(sentry.captureException).toHaveBeenCalledWith(error); expect(sentry.captureMessage).not.toHaveBeenCalled(); expect(sentry.logger.info).not.toHaveBeenCalled(); expect(scope.setLevel).toHaveBeenCalledWith("error"); expect(scope.setTag).toHaveBeenCalledWith("logger.namespace", "API"); expect(scope.setExtras).toHaveBeenCalledWith({ "logger.namespace": "API", requestId: "req_1", }); }); it("writes non-error events to Sentry Logs when enabled, not as Issues", () => { const { sentry } = createFakeSentry(); const logger = createLogger({ environment: "production", namespace: "API", sinks: [createSentrySink(sentry, { logs: true })], }); logger.warn("slow", { ms: 1200 }); logger.info("forced", { id: 1 }, { sentry: true }); expect(sentry.captureMessage).not.toHaveBeenCalled(); expect(sentry.captureException).not.toHaveBeenCalled(); expect(sentry.logger.warn).toHaveBeenCalledWith("slow", { "logger.namespace": "API", ms: 1200, }); expect(sentry.logger.info).toHaveBeenCalledWith("forced", { "logger.namespace": "API", id: 1, }); }); it("does not create an Issue when suppressSentry is set", () => { const { sentry } = createFakeSentry(); const logger = createLogger({ environment: "production", runtime: "node", sinks: [createSentrySink(sentry, { logs: true })], }); logger.error("expected", new Error("nope"), { suppressSentry: true }); expect(sentry.captureException).not.toHaveBeenCalled(); expect(sentry.logger.error).not.toHaveBeenCalled(); }); it("never sends in development", () => { const { sentry } = createFakeSentry(); const logger = createLogger({ environment: "development", sinks: [createSentrySink(sentry, { logs: true })], }); logger.error("local", new Error("x")); logger.warn("local warn", { sentry: true }); expect(sentry.captureException).not.toHaveBeenCalled(); expect(sentry.logger.warn).not.toHaveBeenCalled(); }); it("maps warn → warning on the Issues path and keeps warn on the Logs path", () => { const { scope, sentry } = createFakeSentry(); const sink = createSentrySink(sentry, { logs: true }); sink.emit( issueEvent({ level: "warn", arguments: ["degraded"], }), ); expect(scope.setLevel).toHaveBeenCalledWith("warning"); expect(sentry.captureMessage).toHaveBeenCalledWith("degraded", "warning"); expect(sentry.logger.warn).not.toHaveBeenCalled(); sink.emit({ level: "warn", namespace: "API", arguments: ["slow", { ms: 10 }], timestamp: new Date(), environment: "production", sendToSentryLogs: true, sendToSentryIssue: false, sendToConsole: false, }); expect(sentry.logger.warn).toHaveBeenCalledWith("slow", { "logger.namespace": "API", ms: 10, }); }); it("maps error Issue severity as error", () => { const { scope, sentry } = createFakeSentry(); createSentrySink(sentry).emit( issueEvent({ level: "error", arguments: ["failed"], }), ); expect(scope.setLevel).toHaveBeenCalledWith("error"); expect(sentry.captureMessage).toHaveBeenCalledWith("failed", "error"); }); });