fix(sentry): route fatal logs like errors

SENTRY_LOG_LEVELS accepted 'fatal', but SentryService had no fatal override, so
Nest's ConsoleLogger.fatal printed the message and Sentry never saw it (also
before this branch). A fatal log now becomes an event whenever fatal or error is
captured, and is a breadcrumb otherwise. The level list is a Set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ruslan Konviser
2026-09-27 15:29:08 +02:00
co-authored by Claude Opus 5.5
parent 69a293175a
commit f7659fd4fb
4 changed files with 41 additions and 2 deletions
@@ -57,6 +57,19 @@ describe('SentryService', () => {
});
});
it('sends fatal logs to Sentry whenever errors are captured', () => {
new SentryService({ dsn: DSN, logLevels: ['error'] }).fatal('database unreachable', 'Bootstrap');
expect(Sentry.captureMessage).toHaveBeenCalledWith(expect.stringContaining('database unreachable'), 'fatal');
});
it('keeps fatal logs as breadcrumbs when neither fatal nor error is captured', () => {
new SentryService({ dsn: DSN, logLevels: ['warn'] }).fatal('database unreachable', 'Bootstrap');
expect(Sentry.captureMessage).not.toHaveBeenCalled();
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith(expect.objectContaining({ level: 'fatal' }));
});
it('captures every level listed, e.g. SENTRY_LOG_LEVELS=error,warn', () => {
const logger = new SentryService({ dsn: DSN, logLevels: ['error', 'warn'] });
@@ -191,6 +191,28 @@ export class SentryService extends ConsoleLogger implements OnApplicationShutdow
}
}
/**
* A fatal log is at least as severe as an error, so it becomes an event whenever `fatal` or `error`
* is captured. Without this override Nest's ConsoleLogger.fatal printed it and Sentry never saw it.
*
* @param message
* @param context
*/
fatal(message: string, context?: string) {
message = `${this.app} ${message}`;
try {
super.fatal(message, context);
if (!this.isEnabled()) return;
if (this.captures('fatal') || this.captures('error')) {
Sentry.captureMessage(message, 'fatal');
} else {
Sentry.addBreadcrumb({ message, level: 'fatal', data: { context } });
}
} catch (err) {
// do nothing to avoid blocking the application
}
}
/**
*
* @returns
@@ -9,6 +9,10 @@ describe('parseSentryLogLevels', () => {
expect(parseSentryLogLevels(' Error , WARN ')).toEqual(['error', 'warn']);
});
it('accepts fatal, which SentryService routes like error', () => {
expect(parseSentryLogLevels('fatal')).toEqual(['fatal']);
});
it('ignores unknown names and duplicates but keeps the valid ones', () => {
expect(parseSentryLogLevels('error,info,error,log')).toEqual(['error', 'log']);
});
@@ -1,6 +1,6 @@
import type { LogLevel } from '@nestjs/common';
const NEST_LOG_LEVELS: readonly LogLevel[] = ['log', 'error', 'warn', 'debug', 'verbose', 'fatal'];
const NEST_LOG_LEVELS: ReadonlySet<string> = new Set<LogLevel>(['log', 'error', 'warn', 'debug', 'verbose', 'fatal']);
/**
* The Nest log levels that become Sentry events, from a comma-separated list such as `error,warn`
@@ -12,6 +12,6 @@ export function parseSentryLogLevels(value: string | undefined): LogLevel[] {
const levels = (value ?? '')
.split(',')
.map((level) => level.trim().toLowerCase())
.filter((level): level is LogLevel => NEST_LOG_LEVELS.includes(level as LogLevel));
.filter((level): level is LogLevel => NEST_LOG_LEVELS.has(level));
return levels.length ? [...new Set(levels)] : ['error'];
}