449 lines
12 KiB
TypeScript
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;
|