light-sentry-sdk/src/core/Client.ts

449 lines
12 KiB
TypeScript

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<void> {
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<SentryEvent> & { 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<string, string>): 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<Breadcrumb, 'timestamp'>): 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<void> {
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;