/** * Lightweight Sentry client for CodeGraph — uses the HTTP envelope API directly. * No @sentry/node dependency, works in any Node.js environment. */ import { createHash } from 'crypto'; const DSN = 'https://9591f8aca69bcf98e9feb31544200b47@o1181972.ingest.us.sentry.io/4510840133713920'; const DSN_PARTS = DSN.match(/^https:\/\/([^@]+)@([^/]+)\/(.+)$/); const PUBLIC_KEY = DSN_PARTS![1]; const HOST = DSN_PARTS![2]; const PROJECT_ID = DSN_PARTS![3]; const STORE_URL = `https://${HOST}/api/${PROJECT_ID}/envelope/`; let _enabled = false; let _release = 'codegraph@unknown'; let _tags: Record = {}; /** * Initialize Sentry error reporting. * Safe to call multiple times — subsequent calls update tags/release. */ export function initSentry({ processName, version }: { processName: string; version?: string }) { // Skip in development/test environments if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test' || process.env.VITEST) { return; } _enabled = true; _release = `codegraph@${version ?? process.env.npm_package_version ?? 'unknown'}`; _tags = { processName }; } /** * Send an error to Sentry with full stack trace and context. * Fire-and-forget — never throws, never blocks. */ export function captureException(error: unknown, extra?: Record) { if (!_enabled) return; try { const err = error instanceof Error ? error : new Error(String(error)); const msg = err.message.toLowerCase(); // Filter non-actionable noise if (msg.includes('pty') || msg.includes('terminal session')) return; if ((msg.includes('econnrefused') || msg.includes('econnreset')) && msg.includes('127.0.0.1')) return; const eventId = createHash('md5').update(`${Date.now()}-${Math.random()}`).digest('hex'); const timestamp = new Date().toISOString(); // Attach CodeGraphError context if available const errorContext: Record = { ...extra }; if ('code' in err && typeof (err as any).code === 'string') { errorContext.errorCode = (err as any).code; } if ('context' in err && typeof (err as any).context === 'object') { Object.assign(errorContext, (err as any).context); } const event: Record = { event_id: eventId, timestamp, platform: 'node', level: 'error', release: _release, tags: _tags, exception: { values: [{ type: err.name, value: err.message, stacktrace: parseStack(err.stack), }], }, }; if (Object.keys(errorContext).length > 0) { event.extra = errorContext; } const payload = JSON.stringify(event); const envelope = [ JSON.stringify({ event_id: eventId, sent_at: timestamp, dsn: DSN }), JSON.stringify({ type: 'event', length: payload.length }), payload, ].join('\n') + '\n'; fetch(STORE_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-sentry-envelope', 'X-Sentry-Auth': `Sentry sentry_version=7, sentry_key=${PUBLIC_KEY}`, }, body: envelope, }).catch(() => {}); } catch { // Never throw from error reporting } } /** * Send a message-level event to Sentry (for logged errors without Error objects). */ export function captureMessage(message: string, context?: Record) { if (!_enabled) return; try { const eventId = createHash('md5').update(`${Date.now()}-${Math.random()}`).digest('hex'); const timestamp = new Date().toISOString(); const event: Record = { event_id: eventId, timestamp, platform: 'node', level: 'error', release: _release, tags: _tags, message: { formatted: message }, }; if (context && Object.keys(context).length > 0) { event.extra = context; } const payload = JSON.stringify(event); const envelope = [ JSON.stringify({ event_id: eventId, sent_at: timestamp, dsn: DSN }), JSON.stringify({ type: 'event', length: payload.length }), payload, ].join('\n') + '\n'; fetch(STORE_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-sentry-envelope', 'X-Sentry-Auth': `Sentry sentry_version=7, sentry_key=${PUBLIC_KEY}`, }, body: envelope, }).catch(() => {}); } catch { // Never throw from error reporting } } /** * Parse a Node.js Error.stack string into Sentry's stacktrace format. */ function parseStack(stack?: string): { frames: Array<{ filename: string; function: string; lineno?: number; colno?: number }> } | undefined { if (!stack) return undefined; const frames = stack .split('\n') .slice(1) .map((line) => { const match = line.match(/^\s+at\s+(?:(.+?)\s+\()?(.*?):(\d+):(\d+)\)?$/); if (!match || !match[2] || !match[3] || !match[4]) return null; return { function: match[1] || '', filename: match[2], lineno: parseInt(match[3], 10), colno: parseInt(match[4], 10), }; }) .filter((f): f is NonNullable => f !== null) .reverse(); return frames.length > 0 ? { frames } : undefined; }