| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- /**
- * 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<string, string> = {};
- /**
- * 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<string, unknown>) {
- 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<string, unknown> = { ...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<string, unknown> = {
- 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<string, unknown>) {
- if (!_enabled) return;
- try {
- const eventId = createHash('md5').update(`${Date.now()}-${Math.random()}`).digest('hex');
- const timestamp = new Date().toISOString();
- const event: Record<string, unknown> = {
- 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] || '<anonymous>',
- filename: match[2],
- lineno: parseInt(match[3], 10),
- colno: parseInt(match[4], 10),
- };
- })
- .filter((f): f is NonNullable<typeof f> => f !== null)
- .reverse();
- return frames.length > 0 ? { frames } : undefined;
- }
|