@senzops/apm-node 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.claude/worktrees/infallible-chatelet-f3fb36/.claude/settings.local.json +9 -0
  2. package/.claude/worktrees/infallible-chatelet-f3fb36/CHANGELOG.md +49 -0
  3. package/.claude/worktrees/infallible-chatelet-f3fb36/README.md +398 -0
  4. package/.claude/worktrees/infallible-chatelet-f3fb36/package-lock.json +1494 -0
  5. package/.claude/worktrees/infallible-chatelet-f3fb36/package.json +42 -0
  6. package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/client.ts +451 -0
  7. package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/context.ts +48 -0
  8. package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/normalizer.ts +44 -0
  9. package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/sanitizer.ts +203 -0
  10. package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/transport.ts +273 -0
  11. package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/types.ts +106 -0
  12. package/.claude/worktrees/infallible-chatelet-f3fb36/src/index.ts +36 -0
  13. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/bullmq.ts +195 -0
  14. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/cron.ts +204 -0
  15. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/express.ts +338 -0
  16. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/fastify.ts +296 -0
  17. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/framework.ts +301 -0
  18. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/hook.ts +134 -0
  19. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/http.ts +530 -0
  20. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/koa.ts +173 -0
  21. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/mongo.ts +202 -0
  22. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/mongoose.ts +156 -0
  23. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/mysql.ts +169 -0
  24. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/patch.ts +56 -0
  25. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/pg.ts +131 -0
  26. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/redis.ts +109 -0
  27. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/span.ts +73 -0
  28. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/undici.ts +189 -0
  29. package/.claude/worktrees/infallible-chatelet-f3fb36/src/middleware/express.ts +48 -0
  30. package/.claude/worktrees/infallible-chatelet-f3fb36/src/register.ts +58 -0
  31. package/.claude/worktrees/infallible-chatelet-f3fb36/src/utils/getClientIp.ts +175 -0
  32. package/.claude/worktrees/infallible-chatelet-f3fb36/src/utils/ids.ts +7 -0
  33. package/.claude/worktrees/infallible-chatelet-f3fb36/src/utils/internal.ts +1 -0
  34. package/.claude/worktrees/infallible-chatelet-f3fb36/src/utils/sdkMeta.ts +6 -0
  35. package/.claude/worktrees/infallible-chatelet-f3fb36/src/utils/traceContext.ts +44 -0
  36. package/.claude/worktrees/infallible-chatelet-f3fb36/src/wrappers/fastify.ts +35 -0
  37. package/.claude/worktrees/infallible-chatelet-f3fb36/src/wrappers/h3.ts +59 -0
  38. package/.claude/worktrees/infallible-chatelet-f3fb36/src/wrappers/next.ts +131 -0
  39. package/.claude/worktrees/infallible-chatelet-f3fb36/tsconfig.json +15 -0
  40. package/.claude/worktrees/infallible-chatelet-f3fb36/tsup.config.ts +21 -0
  41. package/.claude/worktrees/infallible-chatelet-f3fb36/wiki.md +852 -0
  42. package/CHANGELOG.md +4 -0
  43. package/dist/index.global.js +1 -1
  44. package/dist/index.global.js.map +1 -1
  45. package/dist/index.js +1 -1
  46. package/dist/index.js.map +1 -1
  47. package/dist/index.mjs +1 -1
  48. package/dist/index.mjs.map +1 -1
  49. package/dist/register.js +1 -1
  50. package/dist/register.js.map +1 -1
  51. package/dist/register.mjs +1 -1
  52. package/dist/register.mjs.map +1 -1
  53. package/package.json +1 -1
  54. package/src/instrumentation/hook.ts +85 -216
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@senzops/apm-node",
3
+ "version": "1.2.1",
4
+ "description": "Universal APM SDK for Senzor",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "require": "./dist/index.js",
11
+ "import": "./dist/index.mjs"
12
+ },
13
+ "./register": {
14
+ "types": "./dist/register.d.ts",
15
+ "require": "./dist/register.js",
16
+ "import": "./dist/register.mjs"
17
+ }
18
+ },
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^20.19.41",
25
+ "tsup": "^8.0.0",
26
+ "typescript": "^5.0.0"
27
+ },
28
+ "engines": {
29
+ "node": ">=18.0.0"
30
+ },
31
+ "keywords": [
32
+ "apm",
33
+ "monitoring",
34
+ "senzor",
35
+ "node",
36
+ "javascript",
37
+ "api",
38
+ "observability"
39
+ ],
40
+ "author": "Senzops",
41
+ "license": "MIT"
42
+ }
@@ -0,0 +1,451 @@
1
+ import { Transport } from './transport';
2
+ import { Context } from './context';
3
+ import { SenzorOptions, ActiveTrace, TaskRun, SenzorLog } from './types';
4
+ import { randomUUID } from 'crypto';
5
+ import { instrumentHttp, instrumentFetch } from '../instrumentation/http';
6
+ import { instrumentExpress } from '../instrumentation/express';
7
+ import { instrumentFastify } from '../instrumentation/fastify';
8
+ import { instrumentKoa } from '../instrumentation/koa';
9
+ import { instrumentMongo } from '../instrumentation/mongo';
10
+ import { instrumentPg } from '../instrumentation/pg';
11
+ import { instrumentUndici } from '../instrumentation/undici';
12
+ import { instrumentRedis } from '../instrumentation/redis';
13
+ import { instrumentMysql } from '../instrumentation/mysql';
14
+ import { instrumentMongoose } from '../instrumentation/mongoose';
15
+ import { instrumentBullMQ } from '../instrumentation/bullmq';
16
+ import { instrumentNodeCron } from '../instrumentation/cron';
17
+ import { SDK_META } from '../utils/sdkMeta';
18
+ import { parseTraceparent } from '../utils/traceContext';
19
+ import { generateSpanId, generateTraceId } from '../utils/ids';
20
+ import { sanitizeAttributes } from './sanitizer';
21
+ import { startCapturedSpan } from '../instrumentation/span';
22
+
23
+ // Memory-safe JSON stringifier to handle cyclical objects
24
+ // (like Express 'req' objects) passed into console.log
25
+ const safeStringify = (obj: any): string => {
26
+ const cache = new Set();
27
+ return JSON.stringify(obj, (key, value) => {
28
+ if (typeof value === 'object' && value !== null) {
29
+ if (cache.has(value)) return '[Circular]';
30
+ cache.add(value);
31
+ }
32
+ return value;
33
+ });
34
+ };
35
+
36
+ export class SenzorClient {
37
+ private transport: Transport | null = null;
38
+ private options: SenzorOptions | null = null;
39
+ private isInstrumented = false;
40
+
41
+ public preload(options: Partial<SenzorOptions> = {}) {
42
+ const endpoint = options.endpoint || 'https://api.senzor.dev/api/ingest/apm';
43
+ const debug = options.debug || false;
44
+
45
+ this.options = {
46
+ apiKey: '',
47
+ ...this.options,
48
+ ...options
49
+ };
50
+
51
+ this.installNativeInstrumentations(endpoint, debug);
52
+ }
53
+
54
+ public init(options: SenzorOptions) {
55
+ if (!options.apiKey) {
56
+ console.warn('[Senzor] API Key missing. SDK disabled.');
57
+ return;
58
+ }
59
+ this.options = options;
60
+ const endpoint = options.endpoint || 'https://api.senzor.dev/api/ingest/apm';
61
+ const debug = options.debug || false;
62
+
63
+ this.transport = new Transport({ ...options, endpoint });
64
+ this.installNativeInstrumentations(endpoint, debug);
65
+ }
66
+
67
+ private isInstrumentationEnabled(name: string): boolean {
68
+ const setting = this.options?.instrumentations;
69
+ if (setting === false) return false;
70
+ if (Array.isArray(setting)) return setting.includes(name);
71
+ return true;
72
+ }
73
+
74
+ private installNativeInstrumentations(endpoint: string, debug: boolean) {
75
+ if (!this.isInstrumented) {
76
+ this.setupGlobalErrorHandlers();
77
+ this.setupLogInterception(); // Fire up Auto Log Instrumentation
78
+
79
+ try { if (this.isInstrumentationEnabled('http')) instrumentHttp(this, endpoint, this.options || undefined); } catch (e) { }
80
+ try { if (this.isInstrumentationEnabled('express')) instrumentExpress(this.options || undefined); } catch (e) { }
81
+ try { if (this.isInstrumentationEnabled('fastify')) instrumentFastify(this.options || undefined); } catch (e) { }
82
+ try { if (this.isInstrumentationEnabled('koa')) instrumentKoa(this.options || undefined); } catch (e) { }
83
+ try { if (this.isInstrumentationEnabled('fetch')) instrumentFetch(endpoint, this.options || undefined); } catch (e) { }
84
+ try { if (this.isInstrumentationEnabled('undici')) instrumentUndici(this.options || undefined); } catch (e) { }
85
+ try { if (this.isInstrumentationEnabled('mongo')) instrumentMongo(this.options || undefined); } catch (e) { }
86
+ try { if (this.isInstrumentationEnabled('mongoose')) instrumentMongoose(this.options || undefined); } catch (e) { }
87
+ try { if (this.isInstrumentationEnabled('pg')) instrumentPg(this.options || undefined); } catch (e) { }
88
+ try { if (this.isInstrumentationEnabled('mysql')) instrumentMysql(this.options || undefined); } catch (e) { }
89
+ try { if (this.isInstrumentationEnabled('redis')) instrumentRedis(this.options || undefined); } catch (e) { }
90
+
91
+ // Task Integrations
92
+ try { if (this.isInstrumentationEnabled('bullmq')) instrumentBullMQ(this, debug); } catch (e) { }
93
+ try { if (this.isInstrumentationEnabled('cron')) instrumentNodeCron(this, debug); } catch (e) { }
94
+
95
+ this.isInstrumented = true;
96
+ if (debug) console.log('[Senzor] Auto-instrumentation enabled');
97
+ }
98
+ }
99
+
100
+ // --- Enterprise Auto-Log Interception ---
101
+ private setupLogInterception() {
102
+ if (this.options?.autoLogs === false) return; // Opt-out check
103
+
104
+ const levels = ['log', 'info', 'warn', 'error', 'debug'] as const;
105
+ const originalConsole = {
106
+ log: console.log,
107
+ info: console.info,
108
+ warn: console.warn,
109
+ error: console.error,
110
+ debug: console.debug
111
+ };
112
+
113
+ let isIntercepting = false; // Lock to prevent SDK internal logs from looping infinitely
114
+
115
+ levels.forEach(level => {
116
+ console[level] = (...args: any[]) => {
117
+ // Always execute original console so user's terminal isn't broken
118
+ originalConsole[level].apply(console, args);
119
+
120
+ if (isIntercepting || !this.transport) return;
121
+ isIntercepting = true;
122
+
123
+ try {
124
+ let message = '';
125
+ let attributes: Record<string, any> = {};
126
+
127
+ args.forEach(arg => {
128
+ if (typeof arg === 'string') {
129
+ message += (message ? ' ' : '') + arg;
130
+ } else if (arg instanceof Error) {
131
+ message += (message ? ' ' : '') + arg.message;
132
+ attributes.errorStack = arg.stack;
133
+ attributes.errorName = arg.name;
134
+ } else if (typeof arg === 'object' && arg !== null) {
135
+ try {
136
+ // New Relic Style Destructuring: Merge all object keys into `attributes`
137
+ const parsed = JSON.parse(safeStringify(arg));
138
+ attributes = { ...attributes, ...sanitizeAttributes(parsed, this.options || undefined) };
139
+ } catch (e) {
140
+ attributes.unparseableObject = true;
141
+ }
142
+ } else {
143
+ message += (message ? ' ' : '') + String(arg);
144
+ }
145
+ });
146
+
147
+ // Fallback if the user purely logged an object without text e.g., console.log({ user: 123 })
148
+ if (!message && Object.keys(attributes).length > 0) {
149
+ message = 'Object Log';
150
+ }
151
+
152
+ // Attach to Active Context seamlessly (Works for BOTH APM and Tasks!)
153
+ const currentTrace = Context.current();
154
+ const logType = currentTrace?.contextType === 'task' ? 'task' : 'apm';
155
+
156
+ const logPayload: SenzorLog = {
157
+ message: message || 'Empty log',
158
+ level: level === 'log' ? 'info' : level, // Map generic log -> info
159
+ attributes,
160
+ timestamp: new Date().toISOString()
161
+ };
162
+
163
+ // Attach the specific contextual ID
164
+ if (currentTrace) {
165
+ if (logType === 'task') logPayload.runId = currentTrace.id;
166
+ else logPayload.traceId = currentTrace.id;
167
+ }
168
+
169
+ this.transport.addLog(logPayload, logType);
170
+ } catch (e) {
171
+ // Absolute failure isolation. Never crash host app during logging.
172
+ } finally {
173
+ isIntercepting = false; // Release lock
174
+ }
175
+ };
176
+ });
177
+ }
178
+
179
+ private setupGlobalErrorHandlers() {
180
+ if ((process as any).__senzorGlobalHandlersInstalled) {
181
+ return;
182
+ }
183
+
184
+ (process as any).__senzorGlobalHandlersInstalled = true;
185
+
186
+ const getProcessContext = () => {
187
+ try {
188
+ return {
189
+ pid: process.pid,
190
+ ppid: process.ppid,
191
+ platform: process.platform,
192
+ uptimeSec: Math.floor(process.uptime()),
193
+ env: process.env.NODE_ENV || 'unknown'
194
+ };
195
+ } catch {
196
+ return {};
197
+ }
198
+ };
199
+
200
+ const getMemoryContext = () => {
201
+ try {
202
+ const mem = process.memoryUsage();
203
+ return {
204
+ rss: mem.rss,
205
+ heapTotal: mem.heapTotal,
206
+ heapUsed: mem.heapUsed,
207
+ external: mem.external,
208
+ arrayBuffers: mem.arrayBuffers
209
+ };
210
+ } catch {
211
+ return {};
212
+ }
213
+ };
214
+
215
+ const safeCapture = (error: unknown, meta: any = {}) => {
216
+ try {
217
+ let parsedError: Error;
218
+ if (error instanceof Error) {
219
+ parsedError = error;
220
+ } else if (typeof error === 'string') {
221
+ parsedError = new Error(error);
222
+ } else {
223
+ try {
224
+ parsedError = new Error(JSON.stringify(error));
225
+ } catch {
226
+ parsedError = new Error('Non-serializable rejection reason');
227
+ }
228
+ }
229
+ const enrichedMeta = {
230
+ ...meta,
231
+ runtime: { name: 'node', version: process.version },
232
+ process: getProcessContext(),
233
+ memory: getMemoryContext(),
234
+ sdk: { name: SDK_META.name, version: SDK_META.version }
235
+ };
236
+
237
+ this.captureError(parsedError, enrichedMeta);
238
+ } catch (internalFailure) {
239
+ try {
240
+ if (this.options?.debug) {
241
+ console.error('[Senzor] Error handler failure:', internalFailure);
242
+ }
243
+ } catch { }
244
+ }
245
+ };
246
+
247
+ process.on('uncaughtExceptionMonitor', (error) => safeCapture(error, { type: 'uncaughtExceptionMonitor', severity: 'fatal' }));
248
+ process.on('uncaughtException', (error) => safeCapture(error, { type: 'uncaughtException', severity: 'fatal' }));
249
+ process.on('unhandledRejection', (reason) => safeCapture(reason, { type: 'unhandledRejection', severity: 'error' }));
250
+ process.on('warning', (warning) => safeCapture(warning, { type: 'processWarning', severity: 'warning' }));
251
+ process.on('multipleResolves', (type, promise, reason) => safeCapture(reason || new Error('Multiple promise resolves'), { type: 'multipleResolves', resolveType: type, severity: 'warning' }));
252
+ process.on('rejectionHandled', (promise) => { if (this.options?.debug) { try { console.warn('[Senzor] rejectionHandled event detected'); } catch { } } });
253
+ process.on('SIGTERM', () => safeCapture(new Error('Process received SIGTERM'), { type: 'processSignal', signal: 'SIGTERM' }));
254
+ process.on('SIGINT', () => safeCapture(new Error('Process received SIGINT'), { type: 'processSignal', signal: 'SIGINT' }));
255
+ }
256
+
257
+ public startTrace<T>(data: Partial<ActiveTrace['data']> & { headers?: any }, next: () => T): T {
258
+ if (!this.transport) return next();
259
+
260
+ const existingTrace = Context.current();
261
+ if (existingTrace?.contextType === 'apm') {
262
+ existingTrace.data = {
263
+ ...existingTrace.data,
264
+ ...data
265
+ };
266
+ return next();
267
+ }
268
+
269
+ let inheritedTraceId: string | undefined = undefined;
270
+ let inheritedParentSpanId: string | undefined = undefined;
271
+
272
+ if (data.headers) {
273
+ const getHeader = (key: string) => {
274
+ if (data.headers[key]) return data.headers[key];
275
+ if (data.headers[key.toLowerCase()]) return data.headers[key.toLowerCase()];
276
+ return undefined;
277
+ };
278
+
279
+ const traceparent = getHeader('traceparent');
280
+ const parsedContext = parseTraceparent(traceparent);
281
+
282
+ if (parsedContext) {
283
+ inheritedTraceId = parsedContext.traceId;
284
+ inheritedParentSpanId = parsedContext.parentSpanId;
285
+ } else {
286
+ const rawTrace = getHeader('x-senzor-trace-id');
287
+ const rawSpan = getHeader('x-senzor-parent-span-id');
288
+ inheritedTraceId = Array.isArray(rawTrace) ? rawTrace[0] : rawTrace;
289
+ inheritedParentSpanId = Array.isArray(rawSpan) ? rawSpan[0] : rawSpan;
290
+ }
291
+ }
292
+
293
+ const activeTraceId = inheritedTraceId || generateTraceId();
294
+ const rootSpanId = generateSpanId();
295
+
296
+ const trace: ActiveTrace = {
297
+ id: activeTraceId,
298
+ contextType: 'apm',
299
+ startTime: performance.now(),
300
+ rootSpanId,
301
+ activeSpanId: rootSpanId,
302
+ data: { ...data, parentTraceId: inheritedTraceId, parentSpanId: inheritedParentSpanId, rootSpanId },
303
+ spans: [],
304
+ maxSpans: this.options?.maxSpansPerTrace ?? 500,
305
+ droppedSpans: 0
306
+ };
307
+
308
+ return Context.run(trace, next);
309
+ }
310
+
311
+ public endTrace(status: number, extraData: any = {}) {
312
+ const trace = Context.current();
313
+ if (!trace || trace.contextType !== 'apm' || !this.transport) return;
314
+ if (trace.ended) return;
315
+ trace.ended = true;
316
+ const duration = performance.now() - trace.startTime;
317
+
318
+ const payload = {
319
+ traceId: trace.id,
320
+ parentTraceId: trace.data.parentTraceId,
321
+ parentSpanId: trace.data.parentSpanId,
322
+ rootSpanId: trace.rootSpanId,
323
+ ...trace.data,
324
+ ...extraData,
325
+ status,
326
+ duration,
327
+ spans: trace.spans,
328
+ droppedSpans: trace.droppedSpans,
329
+ timestamp: new Date().toISOString()
330
+ };
331
+ this.transport.addTrace(payload);
332
+ }
333
+
334
+ // --- TASK MONITORING METHODS ---
335
+ public startTask<T>(name: string, type: 'cron' | 'queue' | 'pipeline' | 'custom', options: any, next: () => T): T {
336
+ if (!this.transport) return next();
337
+
338
+ const currentContext = Context.current();
339
+ const triggerTraceId = currentContext?.contextType === 'apm' ? currentContext.id : undefined;
340
+
341
+ const startMemory = process.memoryUsage ? process.memoryUsage().heapUsed : 0;
342
+ const startCpu = process.cpuUsage ? process.cpuUsage() : undefined;
343
+
344
+ const task: ActiveTrace = {
345
+ id: randomUUID(),
346
+ contextType: 'task',
347
+ startTime: performance.now(),
348
+ rootSpanId: generateSpanId(),
349
+ startMemory,
350
+ startCpu,
351
+ data: { taskName: name, taskType: type, triggerTraceId, ...options },
352
+ spans: [],
353
+ maxSpans: this.options?.maxSpansPerTrace ?? 500,
354
+ droppedSpans: 0
355
+ };
356
+ task.activeSpanId = task.rootSpanId;
357
+ return Context.run(task, next);
358
+ }
359
+
360
+ public endTask(status: 'success' | 'failed', extraMetadata: any = {}) {
361
+ const task = Context.current();
362
+ if (!task || task.contextType !== 'task' || !this.transport) return;
363
+
364
+ let resourceMetrics;
365
+ if (process.memoryUsage && task.startMemory !== undefined && process.cpuUsage && task.startCpu) {
366
+ const endMemory = process.memoryUsage().heapUsed;
367
+ const cpuDelta = process.cpuUsage(task.startCpu);
368
+
369
+ resourceMetrics = {
370
+ memoryDeltaBytes: endMemory - task.startMemory,
371
+ cpuUserUs: cpuDelta.user,
372
+ cpuSystemUs: cpuDelta.system
373
+ };
374
+ }
375
+
376
+ const payload: TaskRun = {
377
+ runId: task.id,
378
+ taskName: task.data.taskName,
379
+ taskType: task.data.taskType,
380
+ triggerTraceId: task.data.triggerTraceId,
381
+ queueDelay: task.data.queueDelay,
382
+ attempts: task.data.attempts,
383
+ isDeadLetter: task.data.isDeadLetter,
384
+ metadata: { ...task.data.metadata, ...extraMetadata, droppedSpans: task.droppedSpans },
385
+ resourceMetrics,
386
+ status,
387
+ duration: performance.now() - task.startTime,
388
+ spans: task.spans,
389
+ timestamp: new Date().toISOString()
390
+ };
391
+
392
+ this.transport.addTask(payload);
393
+ }
394
+
395
+ public wrapTask<T extends (...args: any[]) => any>(name: string, type: 'cron' | 'queue' | 'pipeline' | 'custom', options: any = {}, fn: T): T {
396
+ return (async (...args: any[]) => {
397
+ return this.startTask(name, type, options, async () => {
398
+ try {
399
+ const result = await fn(...args);
400
+ this.endTask('success');
401
+ return result;
402
+ } catch (error) {
403
+ this.captureError(error, { taskName: name });
404
+ this.endTask('failed');
405
+ throw error;
406
+ }
407
+ });
408
+ }) as unknown as T;
409
+ }
410
+
411
+ public captureError(error: unknown, context: any = {}) {
412
+ if (!this.transport) return;
413
+
414
+ let parsedError: Error;
415
+ if (error instanceof Error) {
416
+ parsedError = error;
417
+ } else {
418
+ parsedError = new Error(String(error));
419
+ }
420
+
421
+ const currentTrace = Context.current();
422
+
423
+ const errPayload = {
424
+ errorClass: parsedError.name || 'Error',
425
+ message: parsedError.message,
426
+ stackTrace: parsedError.stack,
427
+ context: sanitizeAttributes(context, this.options || undefined),
428
+ timestamp: new Date().toISOString()
429
+ };
430
+
431
+ if (currentTrace?.contextType === 'task') {
432
+ this.transport.addError({ ...errPayload, runId: currentTrace.id }, 'task');
433
+ } else {
434
+ this.transport.addError({ ...errPayload, traceId: currentTrace?.id }, 'apm');
435
+ }
436
+ }
437
+
438
+ public track(data: any) {
439
+ this.transport?.addTrace({ traceId: generateTraceId(), ...data, spans: [], timestamp: new Date().toISOString() });
440
+ }
441
+
442
+ public startSpan(name: string, type: 'db' | 'http' | 'function' | 'custom' = 'custom') {
443
+ const span = startCapturedSpan(name, type, {}, this.options || undefined);
444
+ if (!span) return { end: () => { } };
445
+ return { end: (meta?: any, status?: number) => span.end(status, meta) };
446
+ }
447
+
448
+ public async flush() { if (this.transport) await this.transport.flush(); }
449
+ }
450
+
451
+ export const client = new SenzorClient();
@@ -0,0 +1,48 @@
1
+ import { AsyncLocalStorage } from 'async_hooks';
2
+ import { ActiveTrace, Span } from './types';
3
+
4
+ export const storage = new AsyncLocalStorage<ActiveTrace>();
5
+
6
+ export const Context = {
7
+ run: <T>(trace: ActiveTrace, fn: () => T): T => {
8
+ return storage.run(trace, fn);
9
+ },
10
+
11
+ withActiveSpan: <T>(spanId: string, fn: () => T): T => {
12
+ const store = storage.getStore();
13
+ if (!store) return fn();
14
+
15
+ return storage.run(
16
+ {
17
+ ...store,
18
+ activeSpanId: spanId,
19
+ data: store.data,
20
+ spans: store.spans
21
+ },
22
+ fn
23
+ );
24
+ },
25
+
26
+ current: (): ActiveTrace | undefined => {
27
+ return storage.getStore();
28
+ },
29
+
30
+ addSpan: (span: Span) => {
31
+ const store = storage.getStore();
32
+ if (store) {
33
+ Context.addSpanToTrace(store, span);
34
+ }
35
+ },
36
+
37
+ addSpanToTrace: (trace: ActiveTrace, span: Span) => {
38
+ if (trace.ended) return;
39
+
40
+ const maxSpans = trace.maxSpans ?? 500;
41
+ if (trace.spans.length >= maxSpans) {
42
+ trace.droppedSpans = (trace.droppedSpans ?? 0) + 1;
43
+ return;
44
+ }
45
+
46
+ trace.spans.push(span);
47
+ }
48
+ };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Heuristic URL Normalizer
3
+ * Converts raw paths with IDs into generic patterns to prevent high cardinality.
4
+ * Example: /users/123/orders/abc-def -> /users/:id/orders/:uuid
5
+ */
6
+ export const normalizePath = (path: string): string => {
7
+ if (!path || path === '/') return '/';
8
+
9
+ return path
10
+ // Replace UUIDs (long alphanumeric strings)
11
+ .replace(
12
+ /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g,
13
+ ':uuid'
14
+ )
15
+ // Replace MongoDB ObjectIds (24 hex chars)
16
+ .replace(/[0-9a-fA-F]{24}/g, ':objectId')
17
+ // Replace pure numeric IDs (e.g., /123)
18
+ .replace(/\/(\d+)(?=\/|$)/g, '/:id')
19
+ // Remove query strings
20
+ .split('?')[0];
21
+ };
22
+
23
+ /**
24
+ * Tries to extract route from Framework internals, falls back to heuristic
25
+ */
26
+ export const getRoute = (req: any, fallbackPath: string): string => {
27
+ // Express / Connect
28
+ if (req.route && req.route.path) {
29
+ return (req.baseUrl || '') + req.route.path;
30
+ }
31
+
32
+ // H3 / Nitro (Nuxt)
33
+ if (req.context && req.context.matchedRoute) {
34
+ return req.context.matchedRoute.path;
35
+ }
36
+
37
+ // Fastify
38
+ if (req.routerPath) {
39
+ return req.routerPath;
40
+ }
41
+
42
+ // Fallback: Heuristic Normalization
43
+ return normalizePath(fallbackPath);
44
+ };