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, }, }); }); it('includes async when the event was deferred for thenables', () => { expect( toSentryLogPayload({ level: 'debug', arguments: ['response'], timestamp: new Date(), environment: 'development', sendToSentryLogs: true, sendToSentryIssue: false, sendToConsole: true, async: true, }).attributes, ).toEqual({ async: 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'); }); });