import { describe, it, expect, vi, beforeEach } from "vitest"; import type { LogRecord } from "@opentelemetry/sdk-logs"; beforeEach(() => vi.resetModules()); // Use raw OTel SeverityNumber values to avoid module-reset issues with the // top-level SeverityNumber import in test.each definitions. // SeverityNumber.INFO = 9, SeverityNumber.WARN = 13, SeverityNumber.ERROR = 17 const SEVERITY_INFO = 9; const SEVERITY_WARN = 13; const SEVERITY_ERROR = 17; describe("createSentryOtelBridge", () => { it("returns a span processor and log record processor when given a DSN", async () => { vi.doMock("@sentry/opentelemetry", () => ({ SentrySpanProcessor: class { onStart() {} onEnd() {} forceFlush() { return Promise.resolve(); } shutdown() { return Promise.resolve(); } }, })); vi.doMock("@sentry/nextjs", () => ({ captureException: vi.fn(), captureMessage: vi.fn(), })); const { createSentryOtelBridge } = await import("./sentry-bridge"); const bridge = createSentryOtelBridge({ dsn: "https://test@sentry.io/1" }); expect(bridge.spanProcessor).toBeDefined(); // Phase 3: logRecordProcessor is now wired (SentryLogRecordForwarder) expect(bridge.logRecordProcessor).toBeDefined(); expect(bridge.logRecordProcessor).not.toBeNull(); }); it("returns null processors when no DSN provided", async () => { const { createSentryOtelBridge } = await import("./sentry-bridge"); const bridge = createSentryOtelBridge({ dsn: "" }); expect(bridge.spanProcessor).toBeNull(); expect(bridge.logRecordProcessor).toBeNull(); }); }); describe("SentryLogRecordForwarder", () => { it("calls Sentry.captureException for ERROR records with exception attributes", async () => { const { SentryLogRecordForwarder } = await import("./sentry-bridge"); const captureException = vi.fn(); const captureMessage = vi.fn(); const forwarder = new SentryLogRecordForwarder({ captureException, captureMessage }); const record = { severityNumber: SEVERITY_ERROR, attributes: { "exception.type": "TypeError", "exception.message": "Cannot read property", "exception.stacktrace": "TypeError: ...\n at foo.ts:10", "tag.feature": "blog", "extra.count": "5", "sentry.fingerprint": "type-a|src-blog", }, body: "Cannot read property", } as unknown as LogRecord; forwarder.onEmit(record); expect(captureException).toHaveBeenCalledTimes(1); const [err, opts] = captureException.mock.calls[0]!; expect(err).toBeInstanceOf(Error); expect(err.name).toBe("TypeError"); expect(err.message).toBe("Cannot read property"); expect(err.stack).toBe("TypeError: ...\n at foo.ts:10"); expect(opts.tags).toEqual({ feature: "blog" }); expect(opts.extra).toEqual({ count: "5" }); expect(opts.fingerprint).toEqual(["type-a", "src-blog"]); expect(captureMessage).not.toHaveBeenCalled(); }); it("calls Sentry.captureException for ERROR records without exception.stacktrace", async () => { const { SentryLogRecordForwarder } = await import("./sentry-bridge"); const captureException = vi.fn(); const captureMessage = vi.fn(); const forwarder = new SentryLogRecordForwarder({ captureException, captureMessage }); const record = { severityNumber: SEVERITY_ERROR, attributes: { "exception.message": "Something failed", }, body: "Something failed", } as unknown as LogRecord; forwarder.onEmit(record); expect(captureException).toHaveBeenCalledTimes(1); expect(captureMessage).not.toHaveBeenCalled(); }); it.each([ [SEVERITY_INFO, "info"], [SEVERITY_WARN, "warning"], ] as const)( "calls Sentry.captureMessage with level '%s' for non-error severity %d", async (severityNumber, expectedLevel) => { const { SentryLogRecordForwarder } = await import("./sentry-bridge"); const captureException = vi.fn(); const captureMessage = vi.fn(); const forwarder = new SentryLogRecordForwarder({ captureException, captureMessage }); const record = { severityNumber, attributes: { "tag.service": "auth", "extra.req": "abc" }, body: "log message", } as unknown as LogRecord; forwarder.onEmit(record); expect(captureMessage).toHaveBeenCalledTimes(1); const [msg, level, opts] = captureMessage.mock.calls[0]!; expect(msg).toBe("log message"); expect(level).toBe(expectedLevel); expect(opts.tags).toEqual({ service: "auth" }); expect(opts.extra).toEqual({ req: "abc" }); expect(captureException).not.toHaveBeenCalled(); }, ); it("forceFlush and shutdown resolve immediately", async () => { const { SentryLogRecordForwarder } = await import("./sentry-bridge"); const forwarder = new SentryLogRecordForwarder({ captureException: vi.fn(), captureMessage: vi.fn(), }); await expect(forwarder.forceFlush()).resolves.toBeUndefined(); await expect(forwarder.shutdown()).resolves.toBeUndefined(); }); });