@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,852 @@
1
+ # Senzor Node APM SDK Wiki
2
+
3
+ This wiki contains detailed integration and operational guidance for `@senzops/apm-node`.
4
+
5
+ The package is the native Senzor SDK for Node.js API services. It captures traces, spans, errors, logs, and task runs without requiring OpenTelemetry in the application.
6
+
7
+ ## 1. Recommended Startup Patterns
8
+
9
+ ### 1.1 Production Preload Mode
10
+
11
+ Preload mode gives the SDK the best chance to capture all supported libraries because instrumentation is installed before the application imports framework, database, queue, and HTTP client modules.
12
+
13
+ CommonJS:
14
+
15
+ ```sh
16
+ SENZOR_API_KEY=sz_apm_your_key_here node -r @senzops/apm-node/register server.js
17
+ ```
18
+
19
+ ESM:
20
+
21
+ ```sh
22
+ SENZOR_API_KEY=sz_apm_your_key_here node --import @senzops/apm-node/register server.mjs
23
+ ```
24
+
25
+ Docker:
26
+
27
+ ```dockerfile
28
+ ENV SENZOR_API_KEY=sz_apm_your_key_here
29
+ CMD ["node", "-r", "@senzops/apm-node/register", "dist/server.js"]
30
+ ```
31
+
32
+ PM2:
33
+
34
+ ```json
35
+ {
36
+ "apps": [
37
+ {
38
+ "name": "orders-api",
39
+ "script": "dist/server.js",
40
+ "node_args": "-r @senzops/apm-node/register",
41
+ "env": {
42
+ "SENZOR_API_KEY": "sz_apm_your_key_here",
43
+ "NODE_ENV": "production"
44
+ }
45
+ }
46
+ ]
47
+ }
48
+ ```
49
+
50
+ ### 1.2 Programmatic Mode
51
+
52
+ Programmatic mode is useful when your hosting platform does not support preload flags.
53
+
54
+ Place `Senzor.init()` at the top of the entrypoint, before importing application modules when possible.
55
+
56
+ ```ts
57
+ import Senzor from '@senzops/apm-node';
58
+
59
+ Senzor.init({
60
+ apiKey: process.env.SENZOR_API_KEY!,
61
+ endpoint: process.env.SENZOR_ENDPOINT,
62
+ batchSize: 100,
63
+ flushInterval: 10000,
64
+ maxSpansPerTrace: 500
65
+ });
66
+
67
+ import './server';
68
+ ```
69
+
70
+ ## 2. Configuration Reference
71
+
72
+ ```ts
73
+ Senzor.init({
74
+ apiKey: 'sz_apm_your_key_here',
75
+ endpoint: 'https://api.senzor.dev',
76
+ batchSize: 100,
77
+ flushInterval: 10000,
78
+ flushTimeoutMs: 5000,
79
+ maxQueueSize: 10000,
80
+ maxSpansPerTrace: 500,
81
+ maxAttributeLength: 2048,
82
+ maxAttributes: 64,
83
+ captureHeaders: false,
84
+ captureDbStatement: true,
85
+ frameworkSpans: true,
86
+ captureMiddlewareSpans: true,
87
+ captureRouterSpans: true,
88
+ captureLifecycleHookSpans: true,
89
+ autoLogs: true,
90
+ debug: false
91
+ });
92
+ ```
93
+
94
+ | Option | Production recommendation |
95
+ | --- | --- |
96
+ | `apiKey` | Always pass through environment or secret manager. Do not hardcode. |
97
+ | `endpoint` | Use `https://api.senzor.dev` unless your Senzor tenant provides a custom ingest URL. |
98
+ | `batchSize` | Start with `100`. Increase for very high-throughput APIs if network overhead matters. |
99
+ | `flushInterval` | Start with `10000` ms. Lower for short-lived processes. |
100
+ | `flushTimeoutMs` | Keep between `3000` and `10000` ms for API services. |
101
+ | `maxQueueSize` | Keep bounded. Increase only after checking memory pressure. |
102
+ | `maxSpansPerTrace` | Keep `500` unless traces are expected to fan out heavily. |
103
+ | `captureHeaders` | Keep `false` unless sanitized headers are required for debugging. |
104
+ | `captureDbStatement` | Keep enabled only if SQL normalization is acceptable for your data policy. |
105
+ | `autoLogs` | Keep enabled if correlated logs are wanted. Disable if another log pipeline already owns logs. |
106
+ | `debug` | Use only during troubleshooting. |
107
+
108
+ Enable only selected instrumentation:
109
+
110
+ ```ts
111
+ Senzor.init({
112
+ apiKey: process.env.SENZOR_API_KEY!,
113
+ instrumentations: ['http', 'fetch', 'pg', 'redis']
114
+ });
115
+ ```
116
+
117
+ Disable auto-instrumentation while keeping manual APIs:
118
+
119
+ ```ts
120
+ Senzor.init({
121
+ apiKey: process.env.SENZOR_API_KEY!,
122
+ instrumentations: false
123
+ });
124
+ ```
125
+
126
+ ## 3. Environment Variables
127
+
128
+ The preload entrypoint supports these variables:
129
+
130
+ ```sh
131
+ SENZOR_API_KEY=sz_apm_your_key_here
132
+ SENZOR_ENDPOINT=https://api.senzor.dev
133
+ SENZOR_BATCH_SIZE=100
134
+ SENZOR_FLUSH_INTERVAL=10000
135
+ SENZOR_FLUSH_TIMEOUT_MS=5000
136
+ SENZOR_MAX_QUEUE_SIZE=10000
137
+ SENZOR_MAX_SPANS_PER_TRACE=500
138
+ SENZOR_CAPTURE_HEADERS=false
139
+ SENZOR_CAPTURE_DB_STATEMENT=true
140
+ SENZOR_FRAMEWORK_SPANS=true
141
+ SENZOR_CAPTURE_MIDDLEWARE_SPANS=true
142
+ SENZOR_CAPTURE_ROUTER_SPANS=true
143
+ SENZOR_CAPTURE_LIFECYCLE_HOOK_SPANS=true
144
+ SENZOR_AUTO_LOGS=true
145
+ SENZOR_DEBUG=false
146
+ ```
147
+
148
+ Accepted API key variables:
149
+
150
+ - `SENZOR_API_KEY`
151
+ - `SENZOR_APM_API_KEY`
152
+ - `SENZOR_SERVICE_API_KEY`
153
+
154
+ Accepted endpoint variables:
155
+
156
+ - `SENZOR_ENDPOINT`
157
+ - `SENZOR_APM_ENDPOINT`
158
+
159
+ ## 4. Framework Integrations
160
+
161
+ ### 4.1 Express and NestJS
162
+
163
+ Preload mode automatically captures inbound HTTP requests. For better Express route names and error capture, add the Senzor middleware.
164
+
165
+ ```ts
166
+ import express from 'express';
167
+ import Senzor from '@senzops/apm-node';
168
+
169
+ Senzor.init({
170
+ apiKey: process.env.SENZOR_API_KEY!
171
+ });
172
+
173
+ const app = express();
174
+
175
+ app.use(Senzor.requestHandler());
176
+
177
+ app.get('/orders/:orderId', async (req, res) => {
178
+ res.json({ orderId: req.params.orderId });
179
+ });
180
+
181
+ app.use(Senzor.errorHandler());
182
+
183
+ app.listen(3000);
184
+ ```
185
+
186
+ For NestJS on Express:
187
+
188
+ ```ts
189
+ import Senzor from '@senzops/apm-node';
190
+ import { NestFactory } from '@nestjs/core';
191
+ import { AppModule } from './app.module';
192
+
193
+ Senzor.init({
194
+ apiKey: process.env.SENZOR_API_KEY!
195
+ });
196
+
197
+ async function bootstrap() {
198
+ const app = await NestFactory.create(AppModule);
199
+ app.use(Senzor.requestHandler());
200
+ await app.listen(3000);
201
+ }
202
+
203
+ bootstrap();
204
+ ```
205
+
206
+ ### 4.2 Fastify
207
+
208
+ ```ts
209
+ import Fastify from 'fastify';
210
+ import Senzor from '@senzops/apm-node';
211
+
212
+ const fastify = Fastify();
213
+
214
+ fastify.register(Senzor.fastifyPlugin, {
215
+ apiKey: process.env.SENZOR_API_KEY!
216
+ });
217
+
218
+ fastify.get('/orders/:orderId', async (request) => {
219
+ return { orderId: request.params.orderId };
220
+ });
221
+
222
+ await fastify.listen({ port: 3000 });
223
+ ```
224
+
225
+ ### 4.3 Next.js App Router
226
+
227
+ ```ts
228
+ import Senzor from '@senzops/apm-node';
229
+
230
+ Senzor.init({
231
+ apiKey: process.env.SENZOR_API_KEY!
232
+ });
233
+
234
+ export const GET = Senzor.wrapNextRoute(async (request: Request) => {
235
+ const url = new URL(request.url);
236
+ return Response.json({ path: url.pathname });
237
+ });
238
+ ```
239
+
240
+ ### 4.4 Next.js Pages Router
241
+
242
+ ```ts
243
+ import type { NextApiRequest, NextApiResponse } from 'next';
244
+ import Senzor from '@senzops/apm-node';
245
+
246
+ Senzor.init({
247
+ apiKey: process.env.SENZOR_API_KEY!
248
+ });
249
+
250
+ export default Senzor.wrapNextPages(
251
+ async function handler(req: NextApiRequest, res: NextApiResponse) {
252
+ res.status(200).json({ ok: true });
253
+ }
254
+ );
255
+ ```
256
+
257
+ For serverless functions:
258
+
259
+ ```ts
260
+ await Senzor.flush();
261
+ ```
262
+
263
+ ### 4.5 H3, Nuxt, Nitro
264
+
265
+ ```ts
266
+ import Senzor from '@senzops/apm-node';
267
+
268
+ Senzor.init({
269
+ apiKey: process.env.SENZOR_API_KEY!
270
+ });
271
+
272
+ export default Senzor.wrapH3(
273
+ defineEventHandler(async (event) => {
274
+ return { ok: true };
275
+ })
276
+ );
277
+ ```
278
+
279
+ ### 4.6 Vanilla Node HTTP
280
+
281
+ ```ts
282
+ import http from 'http';
283
+ import Senzor from '@senzops/apm-node';
284
+
285
+ Senzor.init({
286
+ apiKey: process.env.SENZOR_API_KEY!
287
+ });
288
+
289
+ const server = http.createServer(async (req, res) => {
290
+ res.writeHead(200, { 'content-type': 'application/json' });
291
+ res.end(JSON.stringify({ ok: true }));
292
+ });
293
+
294
+ server.listen(3000);
295
+ ```
296
+
297
+ With preload mode, no manual request wrapper is required.
298
+
299
+ ## 5. Database and Cache Instrumentation
300
+
301
+ ### 5.1 PostgreSQL
302
+
303
+ Supported package: `pg`.
304
+
305
+ ```ts
306
+ import { Pool } from 'pg';
307
+
308
+ const pool = new Pool({
309
+ connectionString: process.env.DATABASE_URL
310
+ });
311
+
312
+ const result = await pool.query(
313
+ 'SELECT id, email FROM users WHERE id = $1',
314
+ [userId]
315
+ );
316
+ ```
317
+
318
+ Captured span example:
319
+
320
+ ```json
321
+ {
322
+ "name": "Postgres SELECT",
323
+ "type": "db",
324
+ "meta": {
325
+ "operation": "SELECT",
326
+ "db.system.name": "postgresql",
327
+ "db.operation.name": "SELECT",
328
+ "db.query.text": "SELECT id, email FROM users WHERE id = $?",
329
+ "rowCount": 1,
330
+ "library": "pg"
331
+ }
332
+ }
333
+ ```
334
+
335
+ ### 5.2 MongoDB
336
+
337
+ Supported package: `mongodb`.
338
+
339
+ ```ts
340
+ const user = await db.collection('users').findOne({ _id: userId });
341
+ const orders = await db.collection('orders').find({ userId }).toArray();
342
+ ```
343
+
344
+ Captured operations include common collection operations and cursor execution:
345
+
346
+ - `findOne`
347
+ - `find`
348
+ - `aggregate`
349
+ - `insertOne`
350
+ - `insertMany`
351
+ - `updateOne`
352
+ - `updateMany`
353
+ - `deleteOne`
354
+ - `deleteMany`
355
+ - `bulkWrite`
356
+ - `countDocuments`
357
+
358
+ ### 5.3 Mongoose
359
+
360
+ Supported package: `mongoose`.
361
+
362
+ ```ts
363
+ const user = await User.findById(userId).exec();
364
+ await user.save();
365
+ ```
366
+
367
+ Captured metadata includes model name, collection name, operation, and driver family.
368
+
369
+ ### 5.4 MySQL and MySQL2
370
+
371
+ Supported packages:
372
+
373
+ - `mysql`
374
+ - `mysql2`
375
+
376
+ ```ts
377
+ const [rows] = await connection.execute(
378
+ 'SELECT id, email FROM users WHERE id = ?',
379
+ [userId]
380
+ );
381
+ ```
382
+
383
+ Captured metadata includes SQL operation, sanitized statement, row count when available, and package name.
384
+
385
+ ### 5.5 Redis and ioredis
386
+
387
+ Supported packages:
388
+
389
+ - `redis`
390
+ - `ioredis`
391
+
392
+ ```ts
393
+ await redis.get(`user:${userId}`);
394
+ await redis.set(`user:${userId}`, JSON.stringify(user));
395
+ ```
396
+
397
+ Captured span names follow the command name:
398
+
399
+ - `Redis GET`
400
+ - `Redis SET`
401
+ - `Redis HGETALL`
402
+ - `Redis DEL`
403
+
404
+ ## 6. HTTP Client Instrumentation
405
+
406
+ The SDK propagates distributed tracing headers on outgoing calls:
407
+
408
+ ```txt
409
+ traceparent: 00-f3b2c2c9c70443f5a4b7f0ff6d5b9a17-9d8a4d5f17e24d2a-01
410
+ x-senzor-trace-id: f3b2c2c9c70443f5a4b7f0ff6d5b9a17
411
+ x-senzor-parent-span-id: 9d8a4d5f17e24d2a
412
+ ```
413
+
414
+ Supported clients:
415
+
416
+ - Node `http.request`
417
+ - Node `https.request`
418
+ - Node `http.get`
419
+ - Node `https.get`
420
+ - Global `fetch`
421
+ - `undici`
422
+
423
+ Example:
424
+
425
+ ```ts
426
+ const response = await fetch('https://inventory.internal/items/sku_123');
427
+ const data = await response.json();
428
+ ```
429
+
430
+ Captured span example:
431
+
432
+ ```json
433
+ {
434
+ "name": "GET inventory.internal",
435
+ "type": "http",
436
+ "status": 200,
437
+ "meta": {
438
+ "method": "GET",
439
+ "library": "fetch",
440
+ "http.request.method": "GET",
441
+ "url.path": "/items/sku_123",
442
+ "server.address": "inventory.internal"
443
+ }
444
+ }
445
+ ```
446
+
447
+ ## 7. Logs and Errors
448
+
449
+ ### 7.1 Correlated Console Logs
450
+
451
+ ```ts
452
+ console.info('order accepted', {
453
+ orderId: 'ord_123',
454
+ customerId: 'cus_456'
455
+ });
456
+ ```
457
+
458
+ Captured log example:
459
+
460
+ ```json
461
+ {
462
+ "message": "order accepted",
463
+ "level": "info",
464
+ "traceId": "f3b2c2c9c70443f5a4b7f0ff6d5b9a17",
465
+ "attributes": {
466
+ "orderId": "ord_123",
467
+ "customerId": "cus_456"
468
+ },
469
+ "timestamp": "2026-05-16T15:30:00.000Z"
470
+ }
471
+ ```
472
+
473
+ Disable console log capture:
474
+
475
+ ```ts
476
+ Senzor.init({
477
+ apiKey: process.env.SENZOR_API_KEY!,
478
+ autoLogs: false
479
+ });
480
+ ```
481
+
482
+ ### 7.2 Manual Error Capture
483
+
484
+ ```ts
485
+ try {
486
+ await chargeCustomer(customerId);
487
+ } catch (error) {
488
+ Senzor.captureException(error, {
489
+ customerId,
490
+ operation: 'charge_customer'
491
+ });
492
+ throw error;
493
+ }
494
+ ```
495
+
496
+ Captured error example:
497
+
498
+ ```json
499
+ {
500
+ "errorClass": "Error",
501
+ "message": "Card declined",
502
+ "traceId": "f3b2c2c9c70443f5a4b7f0ff6d5b9a17",
503
+ "context": {
504
+ "customerId": "cus_456",
505
+ "operation": "charge_customer"
506
+ },
507
+ "timestamp": "2026-05-16T15:30:00.000Z"
508
+ }
509
+ ```
510
+
511
+ Global handlers capture:
512
+
513
+ - `uncaughtExceptionMonitor`
514
+ - `uncaughtException`
515
+ - `unhandledRejection`
516
+ - `warning`
517
+ - `multipleResolves`
518
+ - `SIGTERM`
519
+ - `SIGINT`
520
+
521
+ ## 8. Background Tasks
522
+
523
+ ### 8.1 Manual Task Wrapping
524
+
525
+ ```ts
526
+ const rebuildSearchIndex = Senzor.wrapTask(
527
+ 'rebuild_search_index',
528
+ 'custom',
529
+ {
530
+ metadata: {
531
+ owner: 'search',
532
+ schedule: 'manual'
533
+ }
534
+ },
535
+ async () => {
536
+ await rebuildIndex();
537
+ }
538
+ );
539
+
540
+ await rebuildSearchIndex();
541
+ ```
542
+
543
+ ### 8.2 BullMQ
544
+
545
+ When `bullmq` is installed and auto-instrumentation is enabled, worker jobs are captured as task runs.
546
+
547
+ ```ts
548
+ import { Worker } from 'bullmq';
549
+
550
+ new Worker('billing', async (job) => {
551
+ await sendInvoice(job.data.invoiceId);
552
+ });
553
+ ```
554
+
555
+ Captured task fields include:
556
+
557
+ - `taskName`
558
+ - `taskType`
559
+ - `queueDelay`
560
+ - `attempts`
561
+ - `isDeadLetter`
562
+ - `metadata.jobId`
563
+ - `metadata.queueName`
564
+ - `resourceMetrics`
565
+
566
+ ### 8.3 node-cron
567
+
568
+ ```ts
569
+ import cron from 'node-cron';
570
+
571
+ cron.schedule(
572
+ '*/5 * * * *',
573
+ async () => {
574
+ await syncReports();
575
+ },
576
+ {
577
+ name: 'sync_reports'
578
+ }
579
+ );
580
+ ```
581
+
582
+ The SDK wraps scheduled handlers and sends each execution as a task run.
583
+
584
+ ## 9. Manual Traces and Spans
585
+
586
+ ### 9.1 Manual Trace
587
+
588
+ Use `track()` when the runtime cannot use framework wrappers and you already have timing data.
589
+
590
+ ```ts
591
+ Senzor.track({
592
+ method: 'POST',
593
+ route: '/webhooks/payment',
594
+ path: '/webhooks/payment',
595
+ status: 202,
596
+ duration: 18.7,
597
+ spans: []
598
+ });
599
+ ```
600
+
601
+ ### 9.2 Manual Span
602
+
603
+ ```ts
604
+ const span = Senzor.startSpan('reserve_inventory', 'function');
605
+
606
+ try {
607
+ await reserveInventory(orderId);
608
+ span.end({ orderId }, 200);
609
+ } catch (error) {
610
+ span.end({ orderId, error: String(error) }, 500);
611
+ throw error;
612
+ }
613
+ ```
614
+
615
+ Span types:
616
+
617
+ - `http`
618
+ - `db`
619
+ - `function`
620
+ - `custom`
621
+
622
+ ## 10. Payload Format
623
+
624
+ ### 10.1 APM Ingest
625
+
626
+ Endpoint:
627
+
628
+ ```txt
629
+ POST /api/ingest/apm
630
+ ```
631
+
632
+ Payload:
633
+
634
+ ```json
635
+ {
636
+ "traces": [
637
+ {
638
+ "traceId": "f3b2c2c9c70443f5a4b7f0ff6d5b9a17",
639
+ "parentTraceId": "8cb1f2e2f43e4b2b8b3411c7df81e7e0",
640
+ "parentSpanId": "6c62c908a21d4f49",
641
+ "method": "GET",
642
+ "route": "/orders/:orderId",
643
+ "path": "/orders/ord_123?include=items",
644
+ "status": 200,
645
+ "duration": 53.29,
646
+ "ip": "203.0.113.10",
647
+ "userAgent": "Mozilla/5.0",
648
+ "timestamp": "2026-05-16T15:30:00.000Z",
649
+ "spans": [
650
+ {
651
+ "spanId": "9d8a4d5f17e24d2a",
652
+ "parentSpanId": "91f6c551d5a2403f",
653
+ "name": "GET inventory.internal",
654
+ "type": "http",
655
+ "startTime": 5.41,
656
+ "duration": 22.16,
657
+ "status": 200,
658
+ "meta": {
659
+ "method": "GET",
660
+ "library": "fetch",
661
+ "http.request.method": "GET",
662
+ "server.address": "inventory.internal"
663
+ }
664
+ }
665
+ ]
666
+ }
667
+ ],
668
+ "errors": [],
669
+ "logs": []
670
+ }
671
+ ```
672
+
673
+ ### 10.2 Task Ingest
674
+
675
+ Endpoint:
676
+
677
+ ```txt
678
+ POST /api/ingest/task
679
+ ```
680
+
681
+ Payload:
682
+
683
+ ```json
684
+ {
685
+ "runs": [
686
+ {
687
+ "runId": "3917bd35-b1d6-4e23-a1d2-d969e1a7d6a1",
688
+ "taskName": "billing:send_invoice_email",
689
+ "taskType": "queue",
690
+ "status": "success",
691
+ "duration": 188.3,
692
+ "queueDelay": 92,
693
+ "attempts": 1,
694
+ "triggerTraceId": "f3b2c2c9c70443f5a4b7f0ff6d5b9a17",
695
+ "metadata": {
696
+ "jobId": "142",
697
+ "queueName": "billing"
698
+ },
699
+ "resourceMetrics": {
700
+ "memoryDeltaBytes": 1048576,
701
+ "cpuUserUs": 12000,
702
+ "cpuSystemUs": 3000
703
+ },
704
+ "isDeadLetter": false,
705
+ "spans": [],
706
+ "timestamp": "2026-05-16T15:30:00.000Z"
707
+ }
708
+ ],
709
+ "errors": [],
710
+ "logs": []
711
+ }
712
+ ```
713
+
714
+ ## 11. Security, Privacy, and Cardinality
715
+
716
+ The SDK is conservative by default.
717
+
718
+ Sensitive keys are redacted from attributes, headers, logs, and error context:
719
+
720
+ - `authorization`
721
+ - `cookie`
722
+ - `set-cookie`
723
+ - `password`
724
+ - `passwd`
725
+ - `pwd`
726
+ - `secret`
727
+ - `token`
728
+ - `api-key`
729
+ - `x-api-key`
730
+ - `access-token`
731
+ - `refresh-token`
732
+ - `client-secret`
733
+ - `private-key`
734
+
735
+ Cardinality controls:
736
+
737
+ - URL routes are normalized, for example `/users/123` becomes `/users/:id`.
738
+ - UUID-like IDs and Mongo ObjectIds are normalized in routes.
739
+ - Attribute count and string length are bounded.
740
+ - Span count per trace is bounded.
741
+ - Queue size is bounded.
742
+
743
+ Production recommendations:
744
+
745
+ - Keep `captureHeaders` disabled unless required.
746
+ - Never capture request bodies by default.
747
+ - Avoid putting raw customer data into span names.
748
+ - Use route templates and IDs in metadata, not in operation names.
749
+
750
+ ## 12. Transport Behavior
751
+
752
+ The SDK batches telemetry and sends it asynchronously.
753
+
754
+ Behavior:
755
+
756
+ - Queues are bounded with `maxQueueSize`.
757
+ - Flush occurs on `batchSize` or `flushInterval`.
758
+ - Ingest calls have a timeout with `flushTimeoutMs`.
759
+ - Failed batches are requeued within queue limits.
760
+ - SDK ingest requests are marked as internal so they are not traced recursively.
761
+ - A best-effort flush runs during `beforeExit`.
762
+
763
+ Serverless recommendation:
764
+
765
+ ```ts
766
+ await Senzor.flush();
767
+ ```
768
+
769
+ Call this after the response is prepared but before the function exits when deterministic delivery matters.
770
+
771
+ ## 13. Troubleshooting
772
+
773
+ ### Missing spans
774
+
775
+ Check the startup order first. Preload mode should be used when possible:
776
+
777
+ ```sh
778
+ node -r @senzops/apm-node/register server.js
779
+ ```
780
+
781
+ If programmatic mode is used, initialize before importing `pg`, `mongodb`, `redis`, framework modules, or HTTP clients.
782
+
783
+ ### Duplicate traces
784
+
785
+ The SDK deduplicates active traces in common cases. If duplicates still appear, check whether the app is using both native preload mode and custom manual `track()` calls for the same request.
786
+
787
+ ### No data in Senzor
788
+
789
+ Verify:
790
+
791
+ - `SENZOR_API_KEY` is present.
792
+ - `endpoint` points to the expected Senzor environment.
793
+ - The runtime can reach the ingest endpoint.
794
+ - `Senzor.flush()` is called in short-lived runtimes.
795
+ - `debug: true` shows flush attempts.
796
+
797
+ ### High memory usage
798
+
799
+ Tune:
800
+
801
+ ```ts
802
+ Senzor.init({
803
+ apiKey: process.env.SENZOR_API_KEY!,
804
+ maxQueueSize: 5000,
805
+ maxSpansPerTrace: 250,
806
+ maxAttributes: 32,
807
+ maxAttributeLength: 1024
808
+ });
809
+ ```
810
+
811
+ ### High-cardinality routes
812
+
813
+ Use framework wrappers where possible so route templates are available:
814
+
815
+ ```ts
816
+ app.use(Senzor.requestHandler());
817
+ ```
818
+
819
+ For manual traces, pass normalized routes:
820
+
821
+ ```ts
822
+ Senzor.track({
823
+ method: 'GET',
824
+ route: '/users/:id',
825
+ path: '/users/123',
826
+ status: 200,
827
+ duration: 12.4
828
+ });
829
+ ```
830
+
831
+ ## 14. Build and Publish
832
+
833
+ Build the package:
834
+
835
+ ```sh
836
+ npm run build
837
+ ```
838
+
839
+ The build produces:
840
+
841
+ - CommonJS: `dist/index.js`
842
+ - ESM: `dist/index.mjs`
843
+ - Browser/global bundle: `dist/index.global.js`
844
+ - Preload entrypoint: `dist/register.js` and `dist/register.mjs`
845
+ - Type declarations: `dist/index.d.ts`, `dist/register.d.ts`
846
+
847
+ Before publishing, verify:
848
+
849
+ ```sh
850
+ npx tsc --noEmit
851
+ npm run build
852
+ ```