- Add documentation for recently released changes - Update some exports to include enums
195 lines
6.8 KiB
TypeScript
195 lines
6.8 KiB
TypeScript
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> = {}): 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');
|
|
});
|
|
});
|