import type { LightConfig, LightClient, LightPlugin, DSNInfo, SentryEvent, EventLevel, UserInfo, Breadcrumb, ErrorEvent, TransactionContext, SpanContext, Transaction, Span } from '../types'; import { parseDSN } from '../utils/dsn'; import { now } from '../utils/helper'; import { computeFingerprint } from '../utils/hash'; import { parseStackTrace } from '../utils/stacktrace'; import { logDebug, logError, setDebug } from '../utils/logger'; import { setAnonymousId as setStoredAnonymousId } from '../utils/identity'; import { ConfigManager } from './ConfigManager'; import { EventBus } from './EventBus'; import { EventQueue } from './EventQueue'; import { Reporter } from './Reporter'; import { PluginManager } from './PluginManager'; import { TracingManager } from './TracingManager'; class Client implements LightClient { dsn: DSNInfo; private configManager: ConfigManager; private eventBus: EventBus; private queue: EventQueue; private reporter: Reporter; private pluginManager: PluginManager; private tracingManager: TracingManager; private breadcrumbs: Breadcrumb[] = []; private enabled: boolean = true; private get maxBreadcrumbs(): number { return this.configManager.get('maxBreadcrumbs') ?? 20; } get config(): LightConfig { return this.configManager.getAll(); } constructor(config: LightConfig) { if (!config.dsn) { throw new Error('DSN is required'); } setDebug(config.debug === true); logDebug('Client', 'Initializing SDK', { dsn: config.dsn, debug: config.debug, enabled: config.enabled !== false }); this.dsn = parseDSN(config.dsn); this.configManager = new ConfigManager(config); this.eventBus = new EventBus(); this.reporter = new Reporter( this.dsn, this.configManager.get('maxRetries') ?? 3, this.configManager.get('retryDelay') ?? 1000 ); this.pluginManager = new PluginManager(this); this.tracingManager = new TracingManager(config, this, this.dsn.publicKey); this.queue = new EventQueue( this.configManager.get('maxQueueSize') ?? 100, this.configManager.get('flushInterval') ?? 5000, this.eventBus, async (events) => this.flushEvents(events), (events) => this.syncFlushEvents(events) ); this.enabled = this.configManager.get('enabled') !== false; logDebug('Client', 'SDK initialized', { enabled: this.enabled }); } init(): void { try { if (!this.enabled) return; this.queue.start(); this.emit('ready'); } catch (e) { logError('Client', 'init failed', e); } } private async flushEvents(events: SentryEvent[]): Promise { logDebug('Client', 'flushEvents start', { eventCount: events.length }); const processedEvents: SentryEvent[] = []; for (const event of events) { const processed = this.pluginManager.applyBeforeReport(event); if (processed) { processedEvents.push(processed); } else { logDebug('Client', 'Event filtered by beforeReport plugin', { type: event.type, message: 'message' in event ? event.message : undefined }); } } if (processedEvents.length === 0) { logDebug('Client', 'flushEvents: all events filtered by beforeReport'); return; } let isFirstInBatch = true; const finalEvents: SentryEvent[] = []; for (const event of processedEvents) { const withConfig = this.configManager.applyToEvent(event, isFirstInBatch); isFirstInBatch = false; const beforeSend = this.configManager.get('beforeSend'); const afterBeforeSend = beforeSend ? beforeSend(withConfig) : withConfig; if (!afterBeforeSend) { logDebug('Client', 'Event filtered by beforeSend', { type: event.type }); continue; } finalEvents.push(afterBeforeSend); } if (finalEvents.length === 0) { logDebug('Client', 'flushEvents: all events filtered by beforeSend'); return; } logDebug('Client', 'Reporting events', { count: finalEvents.length }); await this.reporter.report(finalEvents); logDebug('Client', 'Report completed successfully'); this.configManager.markContextReported(); for (const event of finalEvents) { this.pluginManager.applyAfterReport(event); } } private syncFlushEvents(events: SentryEvent[]): void { const processedEvents: SentryEvent[] = []; for (const event of events) { const processed = this.pluginManager.applyBeforeReport(event); if (processed) { processedEvents.push(processed); } } if (processedEvents.length === 0) return; let isFirstInBatch = true; const finalEvents: SentryEvent[] = []; for (const event of processedEvents) { const withConfig = this.configManager.applyToEvent(event, isFirstInBatch); isFirstInBatch = false; const beforeSend = this.configManager.get('beforeSend'); const afterBeforeSend = beforeSend ? beforeSend(withConfig) : withConfig; if (!afterBeforeSend) continue; finalEvents.push(afterBeforeSend); } if (finalEvents.length === 0) return; this.reporter.reportSync(finalEvents); this.configManager.markContextReported(); } on(event: string, handler: (...args: unknown[]) => void): void { this.eventBus.on(event, handler); } off(event: string, handler: (...args: unknown[]) => void): void { this.eventBus.off(event, handler); } emit(event: string, ...args: unknown[]): void { this.eventBus.emit(event, ...args); } captureException(error: Error | unknown): void { try { logDebug('Client', 'captureException called', { error: error instanceof Error ? error.message : String(error) }); if (!this.enabled) { logDebug('Client', 'captureException skipped: SDK disabled'); return; } const errorEvent = this.buildErrorEvent(error); logDebug('Client', 'Error event built', { type: errorEvent.type, message: errorEvent.message, fingerprint: (errorEvent as ErrorEvent).fingerprint }); if (this.configManager.isIgnoredError(errorEvent.message)) { logDebug('Client', 'captureException skipped: error ignored by ignoreErrors'); return; } this.attachTraceContext(errorEvent); errorEvent.breadcrumbs = [...this.breadcrumbs]; logDebug('Client', 'Enqueue error event', { breadcrumbs: this.breadcrumbs.length }); this.queue.enqueue(errorEvent); } catch (e) { logError('Client', 'captureException failed', e); } } captureMessage(message: string, level: EventLevel = 'info'): void { try { if (!this.enabled) return; const event: ErrorEvent = { type: 'error', level, message, timestamp: now(), breadcrumbs: [...this.breadcrumbs], }; if (this.configManager.isIgnoredError(message)) return; this.attachTraceContext(event); this.queue.enqueue(event); } catch (e) { logError('Client', 'captureMessage failed', e); } } captureEvent(event: Partial & { type: string }): void { try { if (!this.enabled) return; const fullEvent = { timestamp: now(), level: 'info' as EventLevel, breadcrumbs: [...this.breadcrumbs], ...event, } as SentryEvent; this.attachTraceContext(fullEvent); this.queue.enqueue(fullEvent); } catch (e) { logError('Client', 'captureEvent failed', e); } } private attachTraceContext(event: SentryEvent): void { const currentSpan = this.tracingManager.getCurrentSpan(); const currentTransaction = this.tracingManager.getCurrentTransaction(); if (currentSpan || currentTransaction) { const traceId = currentSpan?.traceId || currentTransaction?.traceId; const spanId = currentSpan?.spanId || currentTransaction?.spanId; const parentSpanId = currentSpan?.parentSpanId; const op = currentSpan?.op || currentTransaction?.op; const status = currentSpan?.status || currentTransaction?.status; event.contexts = { ...event.contexts, trace: { trace_id: traceId || '', span_id: spanId || '', parent_span_id: parentSpanId, op, status, }, }; } } private buildErrorEvent(error: Error | unknown): ErrorEvent { const timestamp = now(); if (error instanceof Error) { const frames = parseStackTrace(error.stack, { maxFrames: 5 }); const fingerprint = computeFingerprint(error.name, error.message, frames); return { type: 'error', level: 'error', message: error.message, timestamp, exception: { type: error.name, value: error.message, stacktrace: { frames, }, }, fingerprint, }; } const message = String(error); return { type: 'error', level: 'error', message, timestamp, }; } setUser(user: UserInfo | null): void { try { this.configManager.setUser(user); this.tracingManager.updateConfig(this.configManager.getAll()); } catch (e) { logError('Client', 'setUser failed', e); } } setAnonymousId(id: string): void { try { setStoredAnonymousId(id); } catch (e) { logError('Client', 'setAnonymousId failed', e); } } startTransaction(context: TransactionContext): Transaction | null { try { if (!this.enabled) return null; const transaction = this.tracingManager.startTransaction(context); if (transaction && transaction.sampled) { logDebug('Client', 'Transaction started and sampled', { name: transaction.name, traceId: transaction.traceId, }); } return transaction; } catch (e) { logError('Client', 'startTransaction failed', e); return null; } } getCurrentTransaction(): Transaction | null { try { return this.tracingManager.getCurrentTransaction(); } catch (e) { logError('Client', 'getCurrentTransaction failed', e); return null; } } getCurrentSpan(): Span | null { try { return this.tracingManager.getCurrentSpan(); } catch (e) { logError('Client', 'getCurrentSpan failed', e); return null; } } setSpan(span: Span | null): void { try { this.tracingManager.setCurrentSpan(span); } catch (e) { logError('Client', 'setSpan failed', e); } } startSpan(context: SpanContext): Span | null { try { if (!this.enabled) return null; return this.tracingManager.startSpan(context); } catch (e) { logError('Client', 'startSpan failed', e); return null; } } setTag(key: string, value: string): void { try { this.configManager.setTag(key, value); } catch (e) { logError('Client', 'setTag failed', e); } } setTags(tags: Record): void { try { this.configManager.setTags(tags); } catch (e) { logError('Client', 'setTags failed', e); } } setExtra(key: string, value: unknown): void { try { this.configManager.setExtra(key, value); } catch (e) { logError('Client', 'setExtra failed', e); } } addBreadcrumb(breadcrumb: Omit): void { try { const fullBreadcrumb: Breadcrumb = { ...breadcrumb, timestamp: now(), }; this.breadcrumbs.push(fullBreadcrumb); if (this.breadcrumbs.length > this.maxBreadcrumbs) { this.breadcrumbs.shift(); } } catch (e) { logError('Client', 'addBreadcrumb failed', e); } } async flush(): Promise { try { await this.queue.flush(); } catch (e) { logError('Client', 'flush failed', e); } } disable(): void { try { this.enabled = false; } catch (e) { logError('Client', 'disable failed', e); } } enable(): void { try { this.enabled = true; } catch (e) { logError('Client', 'enable failed', e); } } use(plugin: LightPlugin): void { try { this.pluginManager.add(plugin); } catch (e) { logError('Client', 'use plugin failed', e); } } destroy(): void { try { this.pluginManager.destroy(); this.tracingManager.destroy(); this.queue.destroy(); this.eventBus.destroy(); this.breadcrumbs = []; } catch (e) { logError('Client', 'destroy failed', e); } } } export default Client;