Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 1m10s
162 lines
4.7 KiB
TypeScript
162 lines
4.7 KiB
TypeScript
// src/services/sentry.server.ts
|
|
/**
|
|
* Sentry SDK initialization for error tracking.
|
|
* Implements ADR-015: Application Performance Monitoring and Error Tracking.
|
|
*
|
|
* This module configures @sentry/node to send errors to our self-hosted
|
|
* Bugsink instance, which is Sentry-compatible.
|
|
*
|
|
* IMPORTANT: This module should be imported and initialized at the very top
|
|
* of server.ts, before any other imports, to ensure all errors are captured.
|
|
*
|
|
* Note: Uses Sentry SDK v8+ API which differs significantly from v7.
|
|
*/
|
|
import * as Sentry from '@sentry/node';
|
|
import type { Request, Response, NextFunction, ErrorRequestHandler } from 'express';
|
|
import { config, isSentryConfigured, isProduction, isTest } from '../config/env';
|
|
import { logger } from './logger.server';
|
|
|
|
/**
|
|
* Initializes the Sentry SDK with the configured DSN.
|
|
* Should be called once at application startup.
|
|
*/
|
|
export function initSentry(): void {
|
|
if (!isSentryConfigured) {
|
|
logger.info('[Sentry] Error tracking disabled (SENTRY_DSN not configured)');
|
|
return;
|
|
}
|
|
|
|
// Don't initialize Sentry in test environment
|
|
if (isTest) {
|
|
logger.debug('[Sentry] Skipping initialization in test environment');
|
|
return;
|
|
}
|
|
|
|
Sentry.init({
|
|
dsn: config.sentry.dsn,
|
|
environment: config.sentry.environment || config.server.nodeEnv,
|
|
debug: config.sentry.debug,
|
|
|
|
// Performance monitoring - disabled for now to keep it simple
|
|
tracesSampleRate: 0,
|
|
|
|
// Before sending an event, add additional context
|
|
beforeSend(event, hint) {
|
|
// In development, log errors to console as well
|
|
if (!isProduction && hint.originalException) {
|
|
logger.error(
|
|
{ err: hint.originalException, sentryEventId: event.event_id },
|
|
'[Sentry] Capturing error',
|
|
);
|
|
}
|
|
return event;
|
|
},
|
|
});
|
|
|
|
logger.info(
|
|
{ environment: config.sentry.environment || config.server.nodeEnv },
|
|
'[Sentry] Error tracking initialized',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Creates Sentry middleware for Express.
|
|
* Returns the request handler and error handler middleware.
|
|
*
|
|
* In Sentry SDK v8+, the old Handlers.requestHandler and Handlers.errorHandler
|
|
* have been replaced. Request context is now captured automatically via the
|
|
* Express integration. We provide a custom error handler that filters errors.
|
|
*/
|
|
export function getSentryMiddleware(): {
|
|
requestHandler: (req: Request, res: Response, next: NextFunction) => void;
|
|
errorHandler: ErrorRequestHandler;
|
|
} {
|
|
if (!isSentryConfigured || isTest) {
|
|
// Return no-op middleware when Sentry is not configured
|
|
return {
|
|
requestHandler: (_req: Request, _res: Response, next: NextFunction) => next(),
|
|
errorHandler: (_err: Error, _req: Request, _res: Response, next: NextFunction) => next(_err),
|
|
};
|
|
}
|
|
|
|
return {
|
|
// In SDK v8+, request context is captured automatically.
|
|
// This middleware is a placeholder for compatibility.
|
|
requestHandler: (_req: Request, _res: Response, next: NextFunction) => next(),
|
|
|
|
// Custom error handler that captures errors to Sentry
|
|
errorHandler: (err: Error, _req: Request, _res: Response, next: NextFunction) => {
|
|
// Only send 5xx errors to Sentry by default
|
|
const statusCode =
|
|
(err as Error & { statusCode?: number }).statusCode ||
|
|
(err as Error & { status?: number }).status ||
|
|
500;
|
|
|
|
if (statusCode >= 500) {
|
|
Sentry.captureException(err);
|
|
}
|
|
|
|
// Pass the error to the next error handler
|
|
next(err);
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Captures an exception and sends it to Sentry.
|
|
* Use this for errors that are caught and handled gracefully.
|
|
*/
|
|
export function captureException(error: Error, context?: Record<string, unknown>): string | null {
|
|
if (!isSentryConfigured || isTest) {
|
|
return null;
|
|
}
|
|
|
|
if (context) {
|
|
Sentry.setContext('additional', context);
|
|
}
|
|
|
|
return Sentry.captureException(error);
|
|
}
|
|
|
|
/**
|
|
* Captures a message and sends it to Sentry.
|
|
* Use this for non-exception events that should be tracked.
|
|
*/
|
|
export function captureMessage(
|
|
message: string,
|
|
level: Sentry.SeverityLevel = 'info',
|
|
): string | null {
|
|
if (!isSentryConfigured || isTest) {
|
|
return null;
|
|
}
|
|
|
|
return Sentry.captureMessage(message, level);
|
|
}
|
|
|
|
/**
|
|
* Sets the user context for all subsequent events.
|
|
* Call this after user authentication.
|
|
*/
|
|
export function setUser(user: { id: string; email?: string; username?: string } | null): void {
|
|
if (!isSentryConfigured || isTest) {
|
|
return;
|
|
}
|
|
|
|
Sentry.setUser(user);
|
|
}
|
|
|
|
/**
|
|
* Adds a breadcrumb to the current scope.
|
|
* Breadcrumbs are logged actions that led up to an error.
|
|
*/
|
|
export function addBreadcrumb(breadcrumb: Sentry.Breadcrumb): void {
|
|
if (!isSentryConfigured || isTest) {
|
|
return;
|
|
}
|
|
|
|
Sentry.addBreadcrumb(breadcrumb);
|
|
}
|
|
|
|
// Re-export Sentry for advanced usage
|
|
export { Sentry };
|