/** * Sentry integration module * * Initializes Sentry SDK when SENTRY_DSN environment variable is set. * When DSN is not configured, all exports are no-ops. */ import * as Sentry from '@sentry/node' import { logForDebugging } from './debug.js' declare const BUILD_ENV: string | undefined let initialized = false /** * Initialize Sentry SDK. Safe to call multiple times — subsequent calls are no-ops. * Only activates when SENTRY_DSN environment variable is set. */ export function initSentry(): void { if (initialized) { return } const dsn = process.env.SENTRY_DSN if (!dsn) { logForDebugging('[sentry] SENTRY_DSN not set, skipping initialization') return } Sentry.init({ dsn, release: typeof MACRO !== 'undefined' ? MACRO.VERSION : undefined, environment: typeof BUILD_ENV !== 'undefined' ? (BUILD_ENV as string) : process.env.NODE_ENV || 'development', // Limit breadcrumbs and attachments to control payload size maxBreadcrumbs: 20, // Sample rate for error events (1.0 = capture all) sampleRate: 1.0, // Filter sensitive information before sending beforeSend(event) { // Strip auth headers from request data const request = event.request if (request?.headers) { const sensitiveHeaders = [ 'authorization', 'x-api-key', 'cookie', 'set-cookie', ] for (const key of Object.keys(request.headers)) { if (sensitiveHeaders.includes(key.toLowerCase())) { delete request.headers[key] } } } return event }, // Ignore specific error patterns ignoreErrors: [ // Network errors from unreachable hosts — not actionable 'ECONNREFUSED', 'ECONNRESET', 'ENOTFOUND', 'ETIMEDOUT', // User-initiated aborts 'AbortError', 'The user aborted a request', // Interactive cancellation signals 'CancelError', ], beforeSendTransaction(event) { // Don't send performance transactions for now — errors only return null }, }) initialized = true logForDebugging('[sentry] Initialized successfully') } /** * Capture an exception and send it to Sentry. * No-op if Sentry has not been initialized. */ export function captureException( error: unknown, context?: Record, ): void { if (!initialized) { return } try { Sentry.withScope(scope => { if (context) { scope.setExtras(context) } Sentry.captureException(error) }) } catch { // Sentry itself failed — don't let it crash the app } } /** * Set a tag on the current scope for grouping/filtering in Sentry. * No-op if Sentry has not been initialized. */ export function setTag(key: string, value: string): void { if (!initialized) { return } try { Sentry.setTag(key, value) } catch { // Ignore } } /** * Set user context in Sentry for error attribution. * No-op if Sentry has not been initialized. */ export function setUser(user: { id?: string email?: string username?: string }): void { if (!initialized) { return } try { Sentry.setUser(user) } catch { // Ignore } } /** * Flush pending Sentry events and close the client. * Call during graceful shutdown to ensure events are sent. */ export async function closeSentry(timeoutMs = 2000): Promise { if (!initialized) { return } try { await Sentry.close(timeoutMs) logForDebugging('[sentry] Closed successfully') } catch { // Ignore — we're shutting down anyway } } /** * Check if Sentry is initialized. Useful for conditional UI rendering. */ export function isSentryInitialized(): boolean { return initialized }