@senzops/apm-node 1.2.8 → 1.3.1

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 (61) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +527 -398
  3. package/dist/index.d.mts +5 -0
  4. package/dist/index.d.ts +5 -0
  5. package/dist/index.global.js +1 -1
  6. package/dist/index.global.js.map +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +1 -1
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/lambda-handler.d.mts +13 -0
  12. package/dist/lambda-handler.d.ts +13 -0
  13. package/dist/lambda-handler.js +2 -0
  14. package/dist/lambda-handler.js.map +1 -0
  15. package/dist/lambda-handler.mjs +2 -0
  16. package/dist/lambda-handler.mjs.map +1 -0
  17. package/dist/register.js +1 -1
  18. package/dist/register.js.map +1 -1
  19. package/dist/register.mjs +1 -1
  20. package/dist/register.mjs.map +1 -1
  21. package/package.json +6 -1
  22. package/src/core/client.ts +57 -0
  23. package/src/core/transport.ts +20 -3
  24. package/src/core/types.ts +5 -1
  25. package/src/index.ts +4 -0
  26. package/src/instrumentation/amqplib.ts +371 -0
  27. package/src/instrumentation/anthropic.ts +245 -0
  28. package/src/instrumentation/aws-sdk.ts +403 -0
  29. package/src/instrumentation/azure-openai.ts +177 -0
  30. package/src/instrumentation/bunyan.ts +93 -0
  31. package/src/instrumentation/cassandra.ts +367 -0
  32. package/src/instrumentation/cohere.ts +227 -0
  33. package/src/instrumentation/connect.ts +200 -0
  34. package/src/instrumentation/dataloader.ts +291 -0
  35. package/src/instrumentation/dns.ts +220 -0
  36. package/src/instrumentation/firebase.ts +445 -0
  37. package/src/instrumentation/fs.ts +260 -0
  38. package/src/instrumentation/generic-pool.ts +317 -0
  39. package/src/instrumentation/google-genai.ts +426 -0
  40. package/src/instrumentation/graphql.ts +434 -0
  41. package/src/instrumentation/grpc.ts +666 -0
  42. package/src/instrumentation/hapi.ts +257 -0
  43. package/src/instrumentation/kafka.ts +360 -0
  44. package/src/instrumentation/knex.ts +249 -0
  45. package/src/instrumentation/lru-memoizer.ts +175 -0
  46. package/src/instrumentation/memcached.ts +190 -0
  47. package/src/instrumentation/mistral.ts +254 -0
  48. package/src/instrumentation/nestjs.ts +243 -0
  49. package/src/instrumentation/net.ts +171 -0
  50. package/src/instrumentation/openai.ts +281 -0
  51. package/src/instrumentation/pino.ts +170 -0
  52. package/src/instrumentation/restify.ts +213 -0
  53. package/src/instrumentation/runtime.ts +352 -0
  54. package/src/instrumentation/socketio.ts +272 -0
  55. package/src/instrumentation/tedious.ts +509 -0
  56. package/src/instrumentation/winston.ts +149 -0
  57. package/src/lambda-handler.ts +262 -0
  58. package/src/register.ts +22 -3
  59. package/src/wrappers/lambda.ts +417 -0
  60. package/tsup.config.ts +4 -4
  61. package/wiki.md +1693 -852
package/wiki.md CHANGED
@@ -1,852 +1,1693 @@
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
- ```
1
+ # Senzor Node APM SDK — Complete Guide
2
+
3
+ Comprehensive integration, configuration, and operational reference for `@senzops/apm-node`.
4
+
5
+ The SDK captures distributed traces, child spans, errors, logs, background task runs, and runtime metrics from Node.js services and sends them to Senzor. It replaces OpenTelemetry auto-instrumentation with a zero-dependency, Senzor-native alternative.
6
+
7
+ **43 auto-instrumentations. 8 framework wrappers. Zero runtime dependencies.**
8
+
9
+ ---
10
+
11
+ ## Table of Contents
12
+
13
+ 1. [Getting Started](#1-getting-started)
14
+ 2. [Startup Modes](#2-startup-modes)
15
+ 3. [Framework Integration Guide](#3-framework-integration-guide)
16
+ 4. [AWS Lambda Integration](#4-aws-lambda-integration)
17
+ 5. [Cloudflare Workers Integration](#5-cloudflare-workers-integration)
18
+ 6. [Auto-Instrumentation Reference](#6-auto-instrumentation-reference)
19
+ 7. [AI / LLM SDK Instrumentation](#7-ai--llm-sdk-instrumentation)
20
+ 8. [Database & Cache Instrumentation](#8-database--cache-instrumentation)
21
+ 9. [Messaging & Queue Instrumentation](#9-messaging--queue-instrumentation)
22
+ 10. [Cloud Provider Instrumentation](#10-cloud-provider-instrumentation)
23
+ 11. [gRPC, GraphQL & Network](#11-grpc-graphql--network)
24
+ 12. [Log Correlation](#12-log-correlation)
25
+ 13. [Background Task Monitoring](#13-background-task-monitoring)
26
+ 14. [Manual Traces & Spans](#14-manual-traces--spans)
27
+ 15. [Error Tracking](#15-error-tracking)
28
+ 16. [Runtime Metrics](#16-runtime-metrics)
29
+ 17. [Distributed Tracing & Context Propagation](#17-distributed-tracing--context-propagation)
30
+ 18. [Configuration Reference](#18-configuration-reference)
31
+ 19. [Environment Variables](#19-environment-variables)
32
+ 20. [Security, Privacy & Cardinality](#20-security-privacy--cardinality)
33
+ 21. [Transport Behavior](#21-transport-behavior)
34
+ 22. [Ingestion Payload Format](#22-ingestion-payload-format)
35
+ 23. [Deployment Patterns](#23-deployment-patterns)
36
+ 24. [Troubleshooting](#24-troubleshooting)
37
+ 25. [Public API Reference](#25-public-api-reference)
38
+ 26. [Build & Publish](#26-build--publish)
39
+
40
+ ---
41
+
42
+ ## 1. Getting Started
43
+
44
+ ### Install
45
+
46
+ ```sh
47
+ npm install @senzops/apm-node
48
+ ```
49
+
50
+ ### Minimal Setup
51
+
52
+ ```ts
53
+ import Senzor from '@senzops/apm-node';
54
+
55
+ Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
56
+ ```
57
+
58
+ That's it. Once initialized, the SDK automatically instruments all supported libraries that your application imports. No additional configuration or per-library setup is required.
59
+
60
+ ---
61
+
62
+ ## 2. Startup Modes
63
+
64
+ ### 2.1 Preload Mode (Recommended for Production)
65
+
66
+ Preload mode gives the SDK the best coverage because it installs instrumentation hooks before the application imports any library.
67
+
68
+ **CommonJS:**
69
+
70
+ ```sh
71
+ SENZOR_API_KEY=sz_apm_xxx node -r @senzops/apm-node/register server.js
72
+ ```
73
+
74
+ **ESM:**
75
+
76
+ ```sh
77
+ SENZOR_API_KEY=sz_apm_xxx node --import @senzops/apm-node/register server.mjs
78
+ ```
79
+
80
+ **Docker:**
81
+
82
+ ```dockerfile
83
+ ENV SENZOR_API_KEY=sz_apm_xxx
84
+ CMD ["node", "-r", "@senzops/apm-node/register", "dist/server.js"]
85
+ ```
86
+
87
+ **PM2:**
88
+
89
+ ```json
90
+ {
91
+ "apps": [{
92
+ "name": "orders-api",
93
+ "script": "dist/server.js",
94
+ "node_args": "-r @senzops/apm-node/register",
95
+ "env": {
96
+ "SENZOR_API_KEY": "sz_apm_xxx",
97
+ "NODE_ENV": "production"
98
+ }
99
+ }]
100
+ }
101
+ ```
102
+
103
+ ### 2.2 Programmatic Mode
104
+
105
+ When preload flags are unavailable (some serverless platforms, custom runtimes), initialize at the very top of your entrypoint.
106
+
107
+ ```ts
108
+ // init.ts import this FIRST
109
+ import Senzor from '@senzops/apm-node';
110
+
111
+ Senzor.init({
112
+ apiKey: process.env.SENZOR_API_KEY!,
113
+ endpoint: process.env.SENZOR_ENDPOINT,
114
+ batchSize: 100,
115
+ flushInterval: 10000,
116
+ });
117
+
118
+ // Then import your app
119
+ import './server';
120
+ ```
121
+
122
+ ### 2.3 When to Use Which
123
+
124
+ | Scenario | Mode |
125
+ |----------|------|
126
+ | Standard Node.js server (Express, Fastify, Koa, etc.) | Preload |
127
+ | Docker, Kubernetes, PM2 | Preload |
128
+ | Next.js API routes | Programmatic + wrapper |
129
+ | Nuxt / Nitro / H3 | Programmatic + wrapper |
130
+ | Cloudflare Workers | Programmatic + wrapper |
131
+ | AWS Lambda | Programmatic + `wrapLambda`, or Lambda Layer with preload |
132
+ | Serverless without `NODE_OPTIONS` support | Programmatic |
133
+
134
+ ---
135
+
136
+ ## 3. Framework Integration Guide
137
+
138
+ ### 3.1 Express
139
+
140
+ Preload mode captures inbound HTTP automatically. Add middleware for route detection and error capture:
141
+
142
+ ```ts
143
+ import express from 'express';
144
+ import Senzor from '@senzops/apm-node';
145
+
146
+ Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
147
+
148
+ const app = express();
149
+
150
+ // MUST be the first middleware
151
+ app.use(Senzor.requestHandler());
152
+
153
+ app.get('/users/:id', async (req, res) => {
154
+ res.json({ id: req.params.id });
155
+ });
156
+
157
+ // MUST be the last middleware
158
+ app.use(Senzor.errorHandler());
159
+
160
+ app.listen(3000);
161
+ ```
162
+
163
+ ### 3.2 Fastify
164
+
165
+ ```ts
166
+ import Fastify from 'fastify';
167
+ import Senzor from '@senzops/apm-node';
168
+
169
+ const fastify = Fastify();
170
+
171
+ fastify.register(Senzor.fastifyPlugin, {
172
+ apiKey: process.env.SENZOR_API_KEY!,
173
+ });
174
+
175
+ fastify.get('/orders/:orderId', async (request) => {
176
+ return { orderId: request.params.orderId };
177
+ });
178
+
179
+ await fastify.listen({ port: 3000 });
180
+ ```
181
+
182
+ ### 3.3 NestJS
183
+
184
+ NestJS runs on Express (default) or Fastify. For Express:
185
+
186
+ ```ts
187
+ import Senzor from '@senzops/apm-node';
188
+ import { NestFactory } from '@nestjs/core';
189
+ import { AppModule } from './app.module';
190
+
191
+ // Initialize BEFORE creating the Nest app
192
+ Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
193
+
194
+ async function bootstrap() {
195
+ const app = await NestFactory.create(AppModule);
196
+ app.use(Senzor.requestHandler());
197
+ await app.listen(3000);
198
+ }
199
+
200
+ bootstrap();
201
+ ```
202
+
203
+ The NestJS instrumentation auto-captures:
204
+ - Controller and method resolution
205
+ - Guards, Interceptors, Pipes execution
206
+ - Exception filter processing
207
+
208
+ ### 3.4 Koa
209
+
210
+ ```ts
211
+ import Koa from 'koa';
212
+ import Senzor from '@senzops/apm-node';
213
+
214
+ Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
215
+
216
+ const app = new Koa();
217
+
218
+ app.use(async (ctx) => {
219
+ ctx.body = { ok: true };
220
+ });
221
+
222
+ app.listen(3000);
223
+ ```
224
+
225
+ With preload mode, Koa middleware stack is automatically instrumented.
226
+
227
+ ### 3.5 Hapi
228
+
229
+ ```ts
230
+ import Hapi from '@hapi/hapi';
231
+ import Senzor from '@senzops/apm-node';
232
+
233
+ Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
234
+
235
+ const server = Hapi.server({ port: 3000 });
236
+
237
+ server.route({
238
+ method: 'GET',
239
+ path: '/users/{id}',
240
+ handler: (request) => ({ id: request.params.id }),
241
+ });
242
+
243
+ await server.start();
244
+ ```
245
+
246
+ ### 3.6 Restify
247
+
248
+ ```ts
249
+ import restify from 'restify';
250
+ import Senzor from '@senzops/apm-node';
251
+
252
+ Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
253
+
254
+ const server = restify.createServer();
255
+
256
+ server.get('/users/:id', (req, res, next) => {
257
+ res.send({ id: req.params.id });
258
+ next();
259
+ });
260
+
261
+ server.listen(3000);
262
+ ```
263
+
264
+ ### 3.7 Next.js
265
+
266
+ **App Router:**
267
+
268
+ ```ts
269
+ import Senzor from '@senzops/apm-node';
270
+
271
+ Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
272
+
273
+ export const GET = Senzor.wrapNextRoute(async (request: Request) => {
274
+ return Response.json({ ok: true });
275
+ });
276
+ ```
277
+
278
+ **Pages Router:**
279
+
280
+ ```ts
281
+ import Senzor from '@senzops/apm-node';
282
+
283
+ Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
284
+
285
+ export default Senzor.wrapNextPages(async (req, res) => {
286
+ res.status(200).json({ ok: true });
287
+ });
288
+ ```
289
+
290
+ ### 3.8 H3 / Nuxt / Nitro
291
+
292
+ ```ts
293
+ import Senzor from '@senzops/apm-node';
294
+
295
+ Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
296
+
297
+ export default Senzor.wrapH3(defineEventHandler(async (event) => {
298
+ return { ok: true };
299
+ }));
300
+ ```
301
+
302
+ **Nitro Plugin (for Cloudflare Workers preset):**
303
+
304
+ ```ts
305
+ // server/plugins/senzor.ts
306
+ import { Senzor } from '@senzops/apm-node';
307
+
308
+ export default defineNitroPlugin((nitroApp) => {
309
+ Senzor.init({ apiKey: '<YOUR_APM_KEY>' });
310
+ Senzor.nitroPlugin(nitroApp);
311
+ });
312
+ ```
313
+
314
+ ### 3.9 Vanilla Node HTTP Server
315
+
316
+ ```ts
317
+ import http from 'http';
318
+ import Senzor from '@senzops/apm-node';
319
+
320
+ Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
321
+
322
+ const server = http.createServer((req, res) => {
323
+ res.writeHead(200, { 'content-type': 'application/json' });
324
+ res.end(JSON.stringify({ ok: true }));
325
+ });
326
+
327
+ server.listen(3000);
328
+ ```
329
+
330
+ With preload mode, no wrapper is needed — `http.createServer` is automatically instrumented.
331
+
332
+ ---
333
+
334
+ ## 4. AWS Lambda Integration
335
+
336
+ Three deployment methods, from zero-code to code-level:
337
+
338
+ | Method | Code Changes | Setup | Coverage |
339
+ |--------|-------------|-------|----------|
340
+ | **Extension Layer** (recommended) | None | Add layer + set env vars | Full: auto-handler wrapping + all auto-instrumentation |
341
+ | **Handler Wrapper** | Modify handler file | `npm install` + code change | Full |
342
+ | **Preload Layer** | None | Add layer + `NODE_OPTIONS` | Partial: outgoing calls/DB only, no handler wrapping |
343
+
344
+ ### 4.1 Lambda Extension Layer (Zero Code Changes, Recommended)
345
+
346
+ The Extension Layer approach works identically to New Relic and Datadog Lambda Layers. You package `@senzops/apm-node` as a Lambda Layer, point the function's handler to Senzor's auto-wrapper, and set `SENZOR_LAMBDA_HANDLER` to your original handler. No code changes.
347
+
348
+ **How it works:**
349
+
350
+ 1. Lambda invokes `@senzops/apm-node/dist/lambda-handler.handler`
351
+ 2. The auto-wrapper reads `SENZOR_LAMBDA_HANDLER` (e.g., `index.handler`)
352
+ 3. It dynamically loads your original handler module from `LAMBDA_TASK_ROOT`
353
+ 4. It wraps your handler with `wrapLambda()` for full APM coverage
354
+ 5. It re-exports the wrapped function for Lambda to invoke
355
+
356
+ **Step 1: Build the Lambda Layer**
357
+
358
+ ```sh
359
+ mkdir -p senzor-layer/nodejs
360
+ cd senzor-layer/nodejs
361
+ npm init -y
362
+ npm install @senzops/apm-node
363
+ cd ..
364
+ zip -r senzor-apm-layer.zip nodejs/
365
+ ```
366
+
367
+ **Step 2: Publish the Layer**
368
+
369
+ ```sh
370
+ aws lambda publish-layer-version \
371
+ --layer-name senzor-apm-node \
372
+ --zip-file fileb://senzor-apm-layer.zip \
373
+ --compatible-runtimes nodejs18.x nodejs20.x nodejs22.x
374
+ ```
375
+
376
+ **Step 3: Configure Your Function**
377
+
378
+ ```sh
379
+ aws lambda update-function-configuration \
380
+ --function-name my-function \
381
+ --layers <LAYER_ARN> \
382
+ --handler @senzops/apm-node/dist/lambda-handler.handler \
383
+ --environment Variables="{ \
384
+ SENZOR_API_KEY=sz_apm_xxx, \
385
+ SENZOR_LAMBDA_HANDLER=index.handler, \
386
+ NODE_OPTIONS=--require @senzops/apm-node/register \
387
+ }"
388
+ ```
389
+
390
+ | Environment Variable | Required | Description |
391
+ |---------------------|----------|-------------|
392
+ | `SENZOR_API_KEY` | Yes | Your Senzor APM API key |
393
+ | `SENZOR_LAMBDA_HANDLER` | Yes | Original handler path (e.g., `index.handler`, `src/app.myHandler`) |
394
+ | `NODE_OPTIONS` | Recommended | `--require @senzops/apm-node/register` for full preload coverage |
395
+
396
+ The handler path supports nested exports: `SENZOR_LAMBDA_HANDLER=src/handlers.api.get` resolves to `require('src/handlers').api.get`.
397
+
398
+ ### 4.2 Extension Layer with AWS CDK
399
+
400
+ ```ts
401
+ import * as lambda from 'aws-cdk-lib/aws-lambda';
402
+ import * as path from 'path';
403
+
404
+ // Create the Senzor APM Layer
405
+ const senzorLayer = new lambda.LayerVersion(this, 'SenzorApmLayer', {
406
+ code: lambda.Code.fromAsset(path.join(__dirname, 'senzor-layer')),
407
+ compatibleRuntimes: [
408
+ lambda.Runtime.NODEJS_18_X,
409
+ lambda.Runtime.NODEJS_20_X,
410
+ lambda.Runtime.NODEJS_22_X,
411
+ ],
412
+ description: 'Senzor APM Node.js Lambda Extension Layer',
413
+ });
414
+
415
+ // Attach to your Lambda function
416
+ const fn = new lambda.Function(this, 'MyFunction', {
417
+ runtime: lambda.Runtime.NODEJS_20_X,
418
+ // Point handler to Senzor's auto-wrapper
419
+ handler: '@senzops/apm-node/dist/lambda-handler.handler',
420
+ code: lambda.Code.fromAsset('lambda'),
421
+ layers: [senzorLayer],
422
+ environment: {
423
+ SENZOR_API_KEY: senzorApiKey.stringValue,
424
+ // Your original handler path
425
+ SENZOR_LAMBDA_HANDLER: 'index.handler',
426
+ NODE_OPTIONS: '--require @senzops/apm-node/register',
427
+ },
428
+ });
429
+ ```
430
+
431
+ Build the layer directory first:
432
+
433
+ ```sh
434
+ mkdir -p senzor-layer/nodejs && cd senzor-layer/nodejs
435
+ npm init -y && npm install @senzops/apm-node
436
+ ```
437
+
438
+ ### 4.3 Extension Layer with AWS SAM
439
+
440
+ ```yaml
441
+ # template.yaml
442
+ AWSTemplateFormatVersion: '2010-09-09'
443
+ Transform: AWS::Serverless-2016-10-31
444
+
445
+ Globals:
446
+ Function:
447
+ Layers:
448
+ - !Ref SenzorApmLayer
449
+ Environment:
450
+ Variables:
451
+ SENZOR_API_KEY: !Ref SenzorApiKey
452
+ NODE_OPTIONS: '--require @senzops/apm-node/register'
453
+
454
+ Resources:
455
+ SenzorApmLayer:
456
+ Type: AWS::Serverless::LayerVersion
457
+ Properties:
458
+ LayerName: senzor-apm-node
459
+ ContentUri: senzor-layer/
460
+ CompatibleRuntimes:
461
+ - nodejs18.x
462
+ - nodejs20.x
463
+ - nodejs22.x
464
+
465
+ MyFunction:
466
+ Type: AWS::Serverless::Function
467
+ Properties:
468
+ Handler: '@senzops/apm-node/dist/lambda-handler.handler'
469
+ Runtime: nodejs20.x
470
+ CodeUri: src/
471
+ Environment:
472
+ Variables:
473
+ SENZOR_LAMBDA_HANDLER: index.handler
474
+ ```
475
+
476
+ ### 4.4 Extension Layer with Serverless Framework
477
+
478
+ ```yaml
479
+ # serverless.yml
480
+ service: my-service
481
+
482
+ provider:
483
+ name: aws
484
+ runtime: nodejs20.x
485
+ environment:
486
+ SENZOR_API_KEY: ${ssm:/senzor/api-key}
487
+ NODE_OPTIONS: '--require @senzops/apm-node/register'
488
+
489
+ layers:
490
+ senzorApm:
491
+ path: senzor-layer
492
+ compatibleRuntimes:
493
+ - nodejs18.x
494
+ - nodejs20.x
495
+ - nodejs22.x
496
+
497
+ functions:
498
+ api:
499
+ handler: '@senzops/apm-node/dist/lambda-handler.handler'
500
+ layers:
501
+ - !Ref SenzorApmLambdaLayer
502
+ environment:
503
+ SENZOR_LAMBDA_HANDLER: src/handlers/api.handler
504
+ ```
505
+
506
+ ### 4.5 Extension Layer via AWS Console
507
+
508
+ 1. **Create the Layer zip** locally:
509
+ ```sh
510
+ mkdir -p senzor-layer/nodejs && cd senzor-layer/nodejs
511
+ npm init -y && npm install @senzops/apm-node
512
+ cd .. && zip -r senzor-apm-layer.zip nodejs/
513
+ ```
514
+
515
+ 2. **Upload the Layer**: Go to Lambda > Layers > Create layer. Upload `senzor-apm-layer.zip`. Set compatible runtimes to `nodejs18.x`, `nodejs20.x`, `nodejs22.x`.
516
+
517
+ 3. **Attach to your function**: Go to your Lambda function > Layers > Add a layer. Choose "Custom layers" and select `senzor-apm-node`.
518
+
519
+ 4. **Update function configuration**:
520
+ - **Handler**: `@senzops/apm-node/dist/lambda-handler.handler`
521
+ - **Environment variables**:
522
+ - `SENZOR_API_KEY` = your API key
523
+ - `SENZOR_LAMBDA_HANDLER` = your original handler (e.g., `index.handler`)
524
+ - `NODE_OPTIONS` = `--require @senzops/apm-node/register`
525
+
526
+ ### 4.6 Extension Layer with Terraform
527
+
528
+ ```hcl
529
+ resource "aws_lambda_layer_version" "senzor_apm" {
530
+ filename = "senzor-apm-layer.zip"
531
+ layer_name = "senzor-apm-node"
532
+ compatible_runtimes = ["nodejs18.x", "nodejs20.x", "nodejs22.x"]
533
+ description = "Senzor APM Node.js Lambda Extension Layer"
534
+ }
535
+
536
+ resource "aws_lambda_function" "api" {
537
+ function_name = "my-function"
538
+ runtime = "nodejs20.x"
539
+ handler = "@senzops/apm-node/dist/lambda-handler.handler"
540
+ filename = "function.zip"
541
+ role = aws_iam_role.lambda.arn
542
+
543
+ layers = [aws_lambda_layer_version.senzor_apm.arn]
544
+
545
+ environment {
546
+ variables = {
547
+ SENZOR_API_KEY = var.senzor_api_key
548
+ SENZOR_LAMBDA_HANDLER = "index.handler"
549
+ NODE_OPTIONS = "--require @senzops/apm-node/register"
550
+ }
551
+ }
552
+ }
553
+ ```
554
+
555
+ ### 4.7 Code-Level Handler Wrapper
556
+
557
+ When you prefer code-level control or cannot use Lambda Layers:
558
+
559
+ ```ts
560
+ import Senzor from '@senzops/apm-node';
561
+
562
+ Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
563
+
564
+ export const handler = Senzor.wrapLambda(async (event, context) => {
565
+ // Your Lambda logic
566
+ return { statusCode: 200, body: JSON.stringify({ ok: true }) };
567
+ });
568
+ ```
569
+
570
+ ### 4.8 What Gets Captured
571
+
572
+ Both the Extension Layer and code-level wrapper capture the same telemetry:
573
+
574
+ **Cold Start Detection:**
575
+ The first invocation in each container is tagged with `faas.coldstart: true`. Subsequent warm invocations are tagged `false`.
576
+
577
+ **Trigger-Type Detection:**
578
+ The wrapper inspects the event shape and automatically detects:
579
+
580
+ | Trigger | Detection |
581
+ |---------|-----------|
582
+ | API Gateway v1 (REST API) | `event.httpMethod` or `event.requestContext.httpMethod` |
583
+ | API Gateway v2 (HTTP API) | `event.requestContext.http.method` |
584
+ | Application Load Balancer | `event.requestContext.elb` |
585
+ | SQS | `event.Records[0].eventSource === 'aws:sqs'` |
586
+ | SNS | `event.Records[0].EventSource === 'aws:sns'` |
587
+ | DynamoDB Streams | `event.Records[0].eventSource === 'aws:dynamodb'` |
588
+ | S3 | `event.Records[0].eventSource === 'aws:s3'` |
589
+ | EventBridge | `event.source` + `event.detail-type` + `event.detail` |
590
+ | Scheduled (CloudWatch Events) | `event.source === 'aws.events'` |
591
+
592
+ For HTTP triggers (API Gateway, ALB), the wrapper extracts method, path, headers, client IP, and status code. For messaging triggers, it extracts queue/topic names, batch sizes, and table names.
593
+
594
+ **Lambda Context Extraction:**
595
+
596
+ | Attribute | Source |
597
+ |-----------|--------|
598
+ | `faas.name` | `context.functionName` |
599
+ | `faas.version` | `context.functionVersion` |
600
+ | `faas.execution` | `context.awsRequestId` |
601
+ | `faas.max_memory` | `context.memoryLimitInMB` |
602
+ | `faas.coldstart` | Module-level boolean flag |
603
+ | `faas.trigger` | `http`, `pubsub`, `datasource`, `timer`, `other` |
604
+ | `cloud.provider` | `aws` |
605
+ | `cloud.platform` | `aws_lambda` |
606
+ | `cloud.region` | `AWS_REGION` env var |
607
+ | `cloud.account.id` | Parsed from invoked ARN |
608
+ | `cloud.resource_id` | `context.invokedFunctionArn` |
609
+ | `aws.log.group.names` | `context.logGroupName` |
610
+
611
+ **Forced Flush:**
612
+ After every invocation, the wrapper calls `await Senzor.flush()` before returning to the Lambda runtime. Lambda freezes the process immediately after the handler returns.
613
+
614
+ **Lambda Extensions API:**
615
+ The wrapper registers as an internal Lambda extension to receive `SHUTDOWN` lifecycle events. This provides a safety-net flush when the execution environment is being terminated.
616
+
617
+ ### 4.9 Lambda Auto-Detection
618
+
619
+ When running inside Lambda (detected via `AWS_LAMBDA_FUNCTION_NAME` env var), the SDK automatically optimizes settings:
620
+
621
+ | Setting | Default | Lambda Override |
622
+ |---------|---------|-----------------|
623
+ | `runtimeMetrics` | `true` | `false` (meaningless per-invocation) |
624
+ | `batchSize` | `100` | `10` (short-lived invocations) |
625
+ | `flushInterval` | `10000` | `0` (flush on demand only) |
626
+
627
+ ---
628
+
629
+ ## 5. Cloudflare Workers Integration
630
+
631
+ ### 5.1 Direct Worker
632
+
633
+ ```ts
634
+ import Senzor from '@senzops/apm-node';
635
+
636
+ Senzor.init({ apiKey: '<YOUR_APM_KEY>' });
637
+
638
+ export default {
639
+ fetch: Senzor.worker(async (request, env, ctx) => {
640
+ const url = new URL(request.url);
641
+ return new Response(JSON.stringify({ path: url.pathname }), {
642
+ headers: { 'content-type': 'application/json' },
643
+ });
644
+ }),
645
+ };
646
+ ```
647
+
648
+ The worker wrapper automatically:
649
+ - Starts a trace with method, path, headers, and client IP
650
+ - Captures response status code
651
+ - Uses `ctx.waitUntil()` for non-blocking flush when available
652
+ - Falls back to `await flush()` otherwise
653
+
654
+ ### 5.2 Nitro + Cloudflare Workers
655
+
656
+ ```ts
657
+ // server/plugins/senzor.ts
658
+ import { Senzor } from '@senzops/apm-node';
659
+
660
+ export default defineNitroPlugin((nitroApp) => {
661
+ Senzor.init({ apiKey: '<YOUR_APM_KEY>' });
662
+ Senzor.nitroPlugin(nitroApp);
663
+ });
664
+ ```
665
+
666
+ ---
667
+
668
+ ## 6. Auto-Instrumentation Reference
669
+
670
+ Complete table of all 43+ auto-instrumentations. All activate automatically when the library is present in `node_modules` and imported by your application.
671
+
672
+ ### Web Frameworks & HTTP (10)
673
+
674
+ | Instrumentation Key | npm Package(s) | Captured Signals |
675
+ |---------------------|----------------|------------------|
676
+ | `http` | Node built-in `http`/`https` | Inbound requests, outbound calls, status codes, timing |
677
+ | `fetch` | Global `fetch` | Outbound HTTP, W3C Traceparent propagation |
678
+ | `undici` | `undici` | Outbound HTTP via Node's native client |
679
+ | `express` | `express` | Route matching, middleware chain, error capture |
680
+ | `fastify` | `fastify` | Route matching, hooks, lifecycle spans |
681
+ | `koa` | `koa` | Middleware stack, route detection |
682
+ | `nestjs` | `@nestjs/core` | Controllers, Guards, Interceptors, Pipes |
683
+ | `hapi` | `@hapi/hapi` | Route handling, request lifecycle |
684
+ | `restify` | `restify` | Route matching, handler chain |
685
+ | `connect` | `connect` | Middleware stack |
686
+
687
+ ### Databases & Cache (9)
688
+
689
+ | Instrumentation Key | npm Package(s) | Captured Signals |
690
+ |---------------------|----------------|------------------|
691
+ | `pg` | `pg` | Queries, prepared statements, row counts, sanitized SQL |
692
+ | `mongo` | `mongodb` | find, insert, update, delete, aggregate, bulkWrite, cursor |
693
+ | `mongoose` | `mongoose` | Model operations, model/collection names |
694
+ | `mysql` | `mysql`, `mysql2` | Queries, sanitized SQL, row counts |
695
+ | `redis` | `redis`, `ioredis` | All commands (GET, SET, HGETALL, DEL, etc.) |
696
+ | `knex` | `knex` | Query builder, raw queries, transactions, streaming |
697
+ | `tedious` | `tedious` | T-SQL queries, stored procedures, row counts, batch SQL |
698
+ | `cassandra` | `cassandra-driver` | CQL queries, batch operations, prepared statements, consistency |
699
+ | `memcached` | `memcached` | get, set, gets, cas, append, prepend, incr, decr, del, flush |
700
+
701
+ ### Messaging & Queues (5)
702
+
703
+ | Instrumentation Key | npm Package(s) | Captured Signals |
704
+ |---------------------|----------------|------------------|
705
+ | `kafka` | `kafkajs` | Producer send, consumer processing, topic, partition, offset |
706
+ | `amqplib` | `amqplib` | Publish, consume, ack/nack, queue, exchange, routing key |
707
+ | `socketio` | `socket.io` | Event emit/receive, namespace, room, acknowledgements |
708
+ | `bullmq` | `bullmq` | Worker jobs as task runs, queue delay, retries, dead-letter |
709
+ | `cron` | `node-cron` | Scheduled jobs as task runs, schedule expression |
710
+
711
+ ### AI / LLM SDKs (7)
712
+
713
+ | Instrumentation Key | npm Package(s) | Captured Signals |
714
+ |---------------------|----------------|------------------|
715
+ | `openai` | `openai` | Chat completions, embeddings, images, audio, assistants, token usage |
716
+ | `anthropic` | `@anthropic-ai/sdk` | Messages, completions, model, input/output tokens, stop reason |
717
+ | `google-genai` | `@google/generative-ai`, `@google-cloud/vertexai` | generateContent, chat, embeddings, countTokens, token usage |
718
+ | `azure-openai` | `@azure/openai` | Chat, completions, embeddings, images, audio (v1.x) |
719
+ | `cohere` | `cohere-ai` | Chat, generate, embed, rerank, classify, summarize, tokenize |
720
+ | `mistral` | `@mistralai/mistralai` | Chat, FIM, embeddings, model, token usage |
721
+ | `aws-sdk` (Bedrock) | `@aws-sdk/client-bedrock-runtime` | InvokeModel, Converse, token usage, model ID, finish reason |
722
+
723
+ ### Cloud & Infrastructure (3)
724
+
725
+ | Instrumentation Key | npm Package(s) | Captured Signals |
726
+ |---------------------|----------------|------------------|
727
+ | `aws-sdk` | `@aws-sdk/*`, `@smithy/smithy-client` | All AWS service calls: S3, DynamoDB, SQS, SNS, Lambda, SES, etc. |
728
+ | `firebase` | `firebase-admin`, `@google-cloud/firestore` | Firestore CRUD/queries, Auth (16 methods), FCM Messaging (9 methods) |
729
+ | `generic-pool` | `generic-pool` | Pool acquire/release, pool size, pending count, queue size |
730
+
731
+ ### Logging (3)
732
+
733
+ | Instrumentation Key | npm Package(s) | Captured Signals |
734
+ |---------------------|----------------|------------------|
735
+ | `pino` | `pino` | Trace/span ID injection into log records |
736
+ | `winston` | `winston` | Trace/span ID injection into transport output |
737
+ | `bunyan` | `bunyan` | Trace/span ID injection into serialized records |
738
+
739
+ ### RPC & Network (4)
740
+
741
+ | Instrumentation Key | npm Package(s) | Captured Signals |
742
+ |---------------------|----------------|------------------|
743
+ | `grpc` | `@grpc/grpc-js` | Unary/streaming calls, service/method, status codes, metadata |
744
+ | `graphql` | `graphql` | Resolver execution, operation name/type, field paths, errors |
745
+ | `dns` | Node built-in `dns` | Lookups, resolve, hostname, record types, timing |
746
+ | `net` | Node built-in `net` | TCP socket connect, data transfer, connection duration |
747
+
748
+ ### Utilities (3)
749
+
750
+ | Instrumentation Key | npm Package(s) | Captured Signals |
751
+ |---------------------|----------------|------------------|
752
+ | `dataloader` | `dataloader` | Batch load calls, batch size, individual key loads |
753
+ | `lru-memoizer` | `lru-memoizer` | Memoized function calls, cache hit/miss |
754
+ | `fs` | Node built-in `fs` | readFile, writeFile, stat, readdir, mkdir, unlink, rename, etc. |
755
+
756
+ ---
757
+
758
+ ## 7. AI / LLM SDK Instrumentation
759
+
760
+ All AI SDK instrumentations follow [OpenTelemetry GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/).
761
+
762
+ ### Common Attributes Captured
763
+
764
+ | Attribute | Description |
765
+ |-----------|-------------|
766
+ | `gen_ai.system` | `openai`, `anthropic`, `google_ai`, `vertex_ai`, `azure_openai`, `cohere`, `mistral`, `aws_bedrock` |
767
+ | `gen_ai.request.model` | Model name from request (e.g., `gpt-4o`, `claude-sonnet-4-20250514`, `gemini-1.5-pro`) |
768
+ | `gen_ai.response.model` | Actual model from response (may differ for aliases) |
769
+ | `gen_ai.operation.name` | `chat`, `embeddings`, `images`, `audio`, `fim`, `rerank`, `classify` |
770
+ | `gen_ai.usage.input_tokens` | Prompt/input token count |
771
+ | `gen_ai.usage.output_tokens` | Completion/output token count |
772
+ | `gen_ai.usage.total_tokens` | Total tokens (when available) |
773
+ | `gen_ai.response.finish_reason` | `stop`, `length`, `tool_calls`, `end_turn`, etc. |
774
+
775
+ ### OpenAI
776
+
777
+ ```ts
778
+ import OpenAI from 'openai';
779
+
780
+ const openai = new OpenAI();
781
+ const response = await openai.chat.completions.create({
782
+ model: 'gpt-4o',
783
+ messages: [{ role: 'user', content: 'Hello' }],
784
+ });
785
+ // Span: "OpenAI chat gpt-4o" with token usage
786
+ ```
787
+
788
+ ### Anthropic
789
+
790
+ ```ts
791
+ import Anthropic from '@anthropic-ai/sdk';
792
+
793
+ const client = new Anthropic();
794
+ const message = await client.messages.create({
795
+ model: 'claude-sonnet-4-20250514',
796
+ max_tokens: 1024,
797
+ messages: [{ role: 'user', content: 'Hello' }],
798
+ });
799
+ // Span: "Anthropic messages claude-sonnet-4-20250514" with input/output tokens
800
+ ```
801
+
802
+ ### Google Gemini
803
+
804
+ ```ts
805
+ import { GoogleGenerativeAI } from '@google/generative-ai';
806
+
807
+ const genai = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY!);
808
+ const model = genai.getGenerativeModel({ model: 'gemini-1.5-pro' });
809
+ const result = await model.generateContent('Hello');
810
+ // Span: "Gemini generateContent gemini-1.5-pro" with token usage
811
+ ```
812
+
813
+ ### Cohere
814
+
815
+ ```ts
816
+ import { CohereClient } from 'cohere-ai';
817
+
818
+ const cohere = new CohereClient({ token: process.env.COHERE_API_KEY! });
819
+ const response = await cohere.chat({ model: 'command-r-plus', message: 'Hello' });
820
+ // Span: "Cohere chat command-r-plus" with billed units
821
+ ```
822
+
823
+ ### Mistral
824
+
825
+ ```ts
826
+ import { Mistral } from '@mistralai/mistralai';
827
+
828
+ const mistral = new Mistral({ apiKey: process.env.MISTRAL_API_KEY! });
829
+ const response = await mistral.chat.complete({
830
+ model: 'mistral-large-latest',
831
+ messages: [{ role: 'user', content: 'Hello' }],
832
+ });
833
+ // Span: "Mistral chat mistral-large-latest" with token usage
834
+ ```
835
+
836
+ ### AWS Bedrock
837
+
838
+ AWS Bedrock calls made through `@aws-sdk/client-bedrock-runtime` are automatically captured with GenAI attributes via the AWS SDK instrumentation. Both InvokeModel and Converse APIs are supported.
839
+
840
+ ```ts
841
+ import { BedrockRuntimeClient, InvokeModelCommand } from '@aws-sdk/client-bedrock-runtime';
842
+
843
+ const client = new BedrockRuntimeClient({ region: 'us-east-1' });
844
+ const response = await client.send(new InvokeModelCommand({
845
+ modelId: 'anthropic.claude-3-sonnet-20240229-v1:0',
846
+ body: JSON.stringify({ messages: [{ role: 'user', content: 'Hello' }] }),
847
+ }));
848
+ // Span: "AWS BedrockRuntime InvokeModel" with gen_ai.* attributes and token usage
849
+ ```
850
+
851
+ ### Azure OpenAI (v1.x)
852
+
853
+ ```ts
854
+ import { OpenAIClient, AzureKeyCredential } from '@azure/openai';
855
+
856
+ const client = new OpenAIClient(endpoint, new AzureKeyCredential(key));
857
+ const response = await client.getChatCompletions('gpt-4-deployment', messages);
858
+ // Span: "Azure OpenAI chat gpt-4-deployment" with token usage
859
+ ```
860
+
861
+ Note: Azure OpenAI SDK v2+ wraps the standard `openai` npm package internally, which is already covered by the OpenAI instrumentation.
862
+
863
+ ---
864
+
865
+ ## 8. Database & Cache Instrumentation
866
+
867
+ ### PostgreSQL (`pg`)
868
+
869
+ ```ts
870
+ const result = await pool.query('SELECT * FROM users WHERE id = $1', [userId]);
871
+ ```
872
+
873
+ Span: `Postgres SELECT` with `db.system.name: 'postgresql'`, `db.operation.name: 'SELECT'`, sanitized SQL, row count.
874
+
875
+ ### MongoDB (`mongodb`)
876
+
877
+ ```ts
878
+ const user = await db.collection('users').findOne({ _id: userId });
879
+ ```
880
+
881
+ Span: `Mongo users findOne` with `db.system.name: 'mongodb'`, `db.collection.name: 'users'`, `db.operation.name: 'findOne'`.
882
+
883
+ ### Mongoose
884
+
885
+ ```ts
886
+ const user = await User.findById(userId).exec();
887
+ ```
888
+
889
+ Span includes model name, collection name, and operation.
890
+
891
+ ### MySQL / MySQL2
892
+
893
+ ```ts
894
+ const [rows] = await connection.execute('SELECT * FROM users WHERE id = ?', [userId]);
895
+ ```
896
+
897
+ Span: `MySQL SELECT` with sanitized SQL and row count.
898
+
899
+ ### Redis / ioredis
900
+
901
+ ```ts
902
+ await redis.get(`user:${userId}`);
903
+ ```
904
+
905
+ Span: `Redis GET` with command name and key.
906
+
907
+ ### Knex
908
+
909
+ ```ts
910
+ const users = await knex('users').where({ active: true }).select('*');
911
+ ```
912
+
913
+ Span: `Knex SELECT users` with query builder context, sanitized SQL, and binding count.
914
+
915
+ ### SQL Server (Tedious)
916
+
917
+ ```ts
918
+ const request = new Request('SELECT * FROM Users WHERE Id = @id', callback);
919
+ request.addParameter('id', TYPES.Int, userId);
920
+ connection.execSql(request);
921
+ ```
922
+
923
+ Span: `SQLServer SELECT` with sanitized T-SQL, row count, and stored procedure name.
924
+
925
+ ### Cassandra
926
+
927
+ ```ts
928
+ const result = await client.execute('SELECT * FROM users WHERE id = ?', [userId], { prepare: true });
929
+ ```
930
+
931
+ Span: `Cassandra SELECT` with `db.system.name: 'cassandra'`, keyspace, consistency level, sanitized CQL.
932
+
933
+ ### Memcached
934
+
935
+ ```ts
936
+ memcached.get('session:abc', (err, data) => { });
937
+ ```
938
+
939
+ Span: `Memcached GET` with command name and key.
940
+
941
+ ---
942
+
943
+ ## 9. Messaging & Queue Instrumentation
944
+
945
+ ### Kafka (kafkajs)
946
+
947
+ ```ts
948
+ // Producer
949
+ await producer.send({ topic: 'orders', messages: [{ value: JSON.stringify(order) }] });
950
+ // Span: "Kafka SEND orders" with topic, partition, message count
951
+
952
+ // Consumer
953
+ await consumer.run({
954
+ eachMessage: async ({ topic, message }) => { /* ... */ },
955
+ });
956
+ // Span: "Kafka RECEIVE orders" with topic, partition, offset
957
+ ```
958
+
959
+ ### RabbitMQ (amqplib)
960
+
961
+ ```ts
962
+ // Publish
963
+ channel.publish('exchange', 'routing.key', Buffer.from(JSON.stringify(data)));
964
+ // Span: "RabbitMQ PUBLISH exchange" with exchange, routing key
965
+
966
+ // Consume
967
+ channel.consume('queue-name', (msg) => { channel.ack(msg); });
968
+ // Span: "RabbitMQ RECEIVE queue-name" with queue name, delivery tag
969
+ ```
970
+
971
+ ### Socket.IO
972
+
973
+ ```ts
974
+ io.on('connection', (socket) => {
975
+ socket.on('message', (data) => { /* ... */ });
976
+ socket.emit('response', { ok: true });
977
+ });
978
+ // Spans for emit and receive with event names, namespace, room
979
+ ```
980
+
981
+ ---
982
+
983
+ ## 10. Cloud Provider Instrumentation
984
+
985
+ ### AWS SDK v3
986
+
987
+ All AWS services are instrumented through the single `Client.send()` dispatch point:
988
+
989
+ ```ts
990
+ import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
991
+
992
+ const s3 = new S3Client({ region: 'us-east-1' });
993
+ await s3.send(new PutObjectCommand({ Bucket: 'my-bucket', Key: 'file.txt', Body: data }));
994
+ // Span: "AWS S3 PutObject" with rpc.service, rpc.method, aws.request_id, aws.region
995
+ ```
996
+
997
+ Covers: S3, DynamoDB, SQS, SNS, Lambda, SES, CloudWatch, Kinesis, EventBridge, Secrets Manager, SSM, STS, IAM, EC2, ECS, EKS, RDS, ElastiCache, Redshift, Cognito, Route53, CloudFront, API Gateway, Step Functions, CodeBuild, KMS, Bedrock Runtime, and any other AWS service using SDK v3.
998
+
999
+ ### Firebase Admin
1000
+
1001
+ **Firestore:**
1002
+
1003
+ ```ts
1004
+ const doc = await db.collection('users').doc('user123').get();
1005
+ await db.collection('users').doc('user123').set({ name: 'Alice' });
1006
+ const snapshot = await db.collection('users').where('active', '==', true).get();
1007
+ // Spans: "Firestore GET users/user123", "Firestore SET users/user123", "Firestore QUERY users"
1008
+ ```
1009
+
1010
+ Captured operations: GET, SET, ADD, UPDATE, DELETE, CREATE, LIST, QUERY, TX_GET, BATCH_COMMIT.
1011
+
1012
+ **Auth:**
1013
+
1014
+ ```ts
1015
+ const user = await admin.auth().getUser(uid);
1016
+ const token = await admin.auth().verifyIdToken(idToken);
1017
+ // Spans: "Firebase Auth getUser", "Firebase Auth verifyIdToken"
1018
+ ```
1019
+
1020
+ 16 methods captured: createUser, getUser, getUserByEmail, getUserByPhoneNumber, listUsers, deleteUser, deleteUsers, updateUser, verifyIdToken, verifySessionCookie, createSessionCookie, revokeRefreshTokens, setCustomUserClaims, generateEmailVerificationLink, generatePasswordResetLink, generateSignInWithEmailLink.
1021
+
1022
+ **FCM Messaging:**
1023
+
1024
+ ```ts
1025
+ await admin.messaging().send({ notification: { title: 'Hi' }, token: deviceToken });
1026
+ // Span: "Firebase FCM send" with success/failure counts
1027
+ ```
1028
+
1029
+ 9 methods captured: send, sendEach, sendEachForMulticast, sendMulticast, sendToDevice, sendToTopic, sendToCondition, subscribeToTopic, unsubscribeFromTopic.
1030
+
1031
+ ---
1032
+
1033
+ ## 11. gRPC, GraphQL & Network
1034
+
1035
+ ### gRPC
1036
+
1037
+ ```ts
1038
+ // Server and client calls automatically instrumented
1039
+ // Span: "grpc /package.Service/Method" with rpc.system, rpc.service, rpc.method, status code
1040
+ ```
1041
+
1042
+ Supports unary, client streaming, server streaming, and bidirectional streaming.
1043
+
1044
+ ### GraphQL
1045
+
1046
+ ```ts
1047
+ // Resolver-level instrumentation
1048
+ // Span: "GraphQL query GetUser" with operation name, operation type, field path
1049
+ ```
1050
+
1051
+ Captures query/mutation/subscription operations and individual field resolver execution.
1052
+
1053
+ ### DNS
1054
+
1055
+ ```ts
1056
+ import dns from 'dns';
1057
+
1058
+ dns.lookup('example.com', (err, address) => { /* ... */ });
1059
+ // Span: "DNS lookup example.com" with hostname, record type
1060
+ ```
1061
+
1062
+ ### Net (TCP Sockets)
1063
+
1064
+ ```ts
1065
+ import net from 'net';
1066
+
1067
+ const socket = net.createConnection({ host: 'db.internal', port: 5432 });
1068
+ // Span: "TCP CONNECT db.internal:5432" with host, port, connection duration
1069
+ ```
1070
+
1071
+ ---
1072
+
1073
+ ## 12. Log Correlation
1074
+
1075
+ ### Console Logs
1076
+
1077
+ All `console.log`, `console.info`, `console.warn`, `console.error`, and `console.debug` calls are automatically captured and correlated with the active trace or task context.
1078
+
1079
+ ```ts
1080
+ app.get('/users/:id', async (req, res) => {
1081
+ console.info('fetching user', { userId: req.params.id });
1082
+ // Log is captured with traceId, level "info", and attributes { userId: "123" }
1083
+ const user = await getUser(req.params.id);
1084
+ res.json(user);
1085
+ });
1086
+ ```
1087
+
1088
+ ### Structured Logging Libraries
1089
+
1090
+ Pino, Winston, and Bunyan get trace/span IDs automatically injected into log records:
1091
+
1092
+ ```ts
1093
+ import pino from 'pino';
1094
+ const logger = pino();
1095
+
1096
+ app.get('/users/:id', (req, res) => {
1097
+ logger.info({ userId: req.params.id }, 'fetching user');
1098
+ // Output includes: traceId, spanId alongside your log fields
1099
+ });
1100
+ ```
1101
+
1102
+ Disable console log capture:
1103
+
1104
+ ```ts
1105
+ Senzor.init({ apiKey: '...', autoLogs: false });
1106
+ ```
1107
+
1108
+ ---
1109
+
1110
+ ## 13. Background Task Monitoring
1111
+
1112
+ ### Auto-Instrumented
1113
+
1114
+ **BullMQ** workers are captured as task runs with queue delay, retry counts, dead-letter detection, and CPU/memory resource metrics.
1115
+
1116
+ **node-cron** scheduled jobs are captured as task runs with the cron schedule expression.
1117
+
1118
+ ### Manual Task Wrapping
1119
+
1120
+ ```ts
1121
+ const processPayment = Senzor.wrapTask(
1122
+ 'process_payment', // task name
1123
+ 'custom', // type: 'cron' | 'queue' | 'pipeline' | 'custom'
1124
+ { metadata: { owner: 'billing' } },
1125
+ async (invoiceId: string) => {
1126
+ await chargeCustomer(invoiceId);
1127
+ }
1128
+ );
1129
+
1130
+ await processPayment('inv_123');
1131
+ ```
1132
+
1133
+ ### Low-Level Task API
1134
+
1135
+ ```ts
1136
+ Senzor.startTask('rebuild_index', 'custom', { metadata: { trigger: 'manual' } }, async () => {
1137
+ try {
1138
+ await rebuildSearchIndex();
1139
+ // endTask is called automatically on success
1140
+ } catch (error) {
1141
+ Senzor.captureException(error);
1142
+ throw error; // endTask('failed') on throw
1143
+ }
1144
+ });
1145
+ ```
1146
+
1147
+ Task payloads include:
1148
+ - `taskName`, `taskType`, `status`, `duration`
1149
+ - `queueDelay`, `attempts`, `isDeadLetter` (for BullMQ)
1150
+ - `resourceMetrics`: heap memory delta, CPU user/system time
1151
+ - `triggerTraceId`: links tasks triggered by HTTP requests
1152
+
1153
+ ---
1154
+
1155
+ ## 14. Manual Traces & Spans
1156
+
1157
+ ### Manual Span
1158
+
1159
+ ```ts
1160
+ const span = Senzor.startSpan('calculate_invoice', 'function');
1161
+ try {
1162
+ const total = await calculateInvoice(invoiceId);
1163
+ span.end({ invoiceId, total }, 200);
1164
+ } catch (error) {
1165
+ span.end({ invoiceId, error: String(error) }, 500);
1166
+ throw error;
1167
+ }
1168
+ ```
1169
+
1170
+ Span types: `http`, `db`, `function`, `custom`, `rpc`, `messaging`, `dns`, `net`.
1171
+
1172
+ ### Manual Trace
1173
+
1174
+ For environments where framework wrappers can't be used:
1175
+
1176
+ ```ts
1177
+ Senzor.track({
1178
+ method: 'POST',
1179
+ route: '/webhooks/payment',
1180
+ path: '/webhooks/payment',
1181
+ status: 202,
1182
+ duration: 18.7,
1183
+ spans: [],
1184
+ });
1185
+ ```
1186
+
1187
+ ---
1188
+
1189
+ ## 15. Error Tracking
1190
+
1191
+ ### Automatic
1192
+
1193
+ The SDK captures these global events:
1194
+
1195
+ | Event | Severity |
1196
+ |-------|----------|
1197
+ | `uncaughtExceptionMonitor` | fatal |
1198
+ | `uncaughtException` | fatal |
1199
+ | `unhandledRejection` | error |
1200
+ | `warning` | warning |
1201
+ | `multipleResolves` | warning |
1202
+ | `SIGTERM` | info |
1203
+ | `SIGINT` | info |
1204
+
1205
+ Each captured error includes: stack trace, process context (pid, platform, uptime, NODE_ENV), memory snapshot (RSS, heap, external), and SDK metadata.
1206
+
1207
+ ### Manual
1208
+
1209
+ ```ts
1210
+ try {
1211
+ await chargeCustomer(customerId);
1212
+ } catch (error) {
1213
+ Senzor.captureException(error, { customerId, operation: 'charge' });
1214
+ throw error;
1215
+ }
1216
+ ```
1217
+
1218
+ Errors are correlated with the active trace or task context.
1219
+
1220
+ ---
1221
+
1222
+ ## 16. Runtime Metrics
1223
+
1224
+ Collected every 15 seconds (configurable) and sent with trace batches.
1225
+
1226
+ | Metric | Description |
1227
+ |--------|-------------|
1228
+ | `eventLoop.lag.p50` | Event loop lag, 50th percentile (ms) |
1229
+ | `eventLoop.lag.p99` | Event loop lag, 99th percentile (ms) |
1230
+ | `eventLoop.lag.max` | Event loop lag, maximum observed (ms) |
1231
+ | `eventLoop.utilization` | Event loop utilization (0-1, from `perf_hooks` ELU) |
1232
+ | `gc.duration.minor` | GC pause time, minor collections (ms) |
1233
+ | `gc.duration.major` | GC pause time, major collections (ms) |
1234
+ | `gc.duration.incremental` | GC pause time, incremental marking (ms) |
1235
+ | `memory.heapUsed` | V8 heap used (bytes) |
1236
+ | `memory.heapTotal` | V8 heap total (bytes) |
1237
+ | `memory.rss` | Resident set size (bytes) |
1238
+ | `memory.external` | External memory (bytes) |
1239
+ | `memory.arrayBuffers` | ArrayBuffer memory (bytes) |
1240
+ | `activeHandles` | Open handles (file descriptors, sockets) |
1241
+ | `activeRequests` | Pending async requests |
1242
+
1243
+ Disable runtime metrics:
1244
+
1245
+ ```ts
1246
+ Senzor.init({ apiKey: '...', runtimeMetrics: false });
1247
+ ```
1248
+
1249
+ Adjust collection interval:
1250
+
1251
+ ```ts
1252
+ Senzor.init({ apiKey: '...', runtimeMetricsInterval: 30000 }); // 30 seconds
1253
+ ```
1254
+
1255
+ Runtime metrics are automatically disabled in AWS Lambda environments.
1256
+
1257
+ ---
1258
+
1259
+ ## 17. Distributed Tracing & Context Propagation
1260
+
1261
+ ### Outgoing Headers
1262
+
1263
+ The SDK injects these headers on all outgoing HTTP calls:
1264
+
1265
+ ```
1266
+ traceparent: 00-{traceId}-{spanId}-01
1267
+ x-senzor-trace-id: {traceId}
1268
+ x-senzor-parent-span-id: {spanId}
1269
+ ```
1270
+
1271
+ ### Incoming Headers
1272
+
1273
+ Incoming requests with `traceparent` or `x-senzor-trace-id` headers are linked as child traces, enabling cross-service distributed trace visualization.
1274
+
1275
+ ### Context Propagation
1276
+
1277
+ The SDK uses `AsyncLocalStorage` to maintain trace context across async boundaries. All child spans, logs, and errors within a request are automatically correlated to the correct trace without any manual threading.
1278
+
1279
+ ---
1280
+
1281
+ ## 18. Configuration Reference
1282
+
1283
+ ```ts
1284
+ Senzor.init({
1285
+ apiKey: 'sz_apm_xxx', // Required
1286
+ endpoint: 'https://api.senzor.dev', // Ingest endpoint
1287
+ batchSize: 100, // Flush threshold
1288
+ flushInterval: 10000, // Flush interval (ms)
1289
+ flushTimeoutMs: 5000, // Per-request timeout (ms)
1290
+ maxQueueSize: 10000, // Max queued items before drop
1291
+ maxSpansPerTrace: 500, // Max spans per trace
1292
+ maxAttributeLength: 2048, // Max string length
1293
+ maxAttributes: 64, // Max attributes per object
1294
+ captureHeaders: false, // Capture sanitized headers
1295
+ captureDbStatement: true, // Capture sanitized SQL
1296
+ instrumentations: true, // true | false | string[]
1297
+ frameworkSpans: true, // Framework middleware/handler spans
1298
+ captureMiddlewareSpans: true, // Middleware execution spans
1299
+ captureRouterSpans: true, // Router dispatch spans
1300
+ captureLifecycleHookSpans: true, // Lifecycle hook spans
1301
+ autoLogs: true, // Console log capture
1302
+ runtimeMetrics: true, // Runtime metrics collection
1303
+ runtimeMetricsInterval: 15000, // Collection interval (ms)
1304
+ debug: false, // SDK diagnostics
1305
+ });
1306
+ ```
1307
+
1308
+ | Option | Production Guidance |
1309
+ |--------|-------------------|
1310
+ | `apiKey` | Always use environment variables or secret manager. Never hardcode. |
1311
+ | `batchSize` | Start at `100`. Increase for high-throughput services. |
1312
+ | `flushInterval` | `10000` ms for servers. `0` for Lambda (flush on demand). |
1313
+ | `maxQueueSize` | Keep bounded. Increase only after checking memory pressure. |
1314
+ | `maxSpansPerTrace` | Keep `500` unless traces fan out heavily (e.g., batch operations). |
1315
+ | `captureHeaders` | Keep `false` unless needed for debugging. |
1316
+ | `debug` | Never enable in production unless actively troubleshooting. |
1317
+
1318
+ ### Selective Instrumentation
1319
+
1320
+ ```ts
1321
+ // Enable only specific instrumentations
1322
+ Senzor.init({
1323
+ apiKey: '...',
1324
+ instrumentations: ['http', 'fetch', 'pg', 'redis', 'openai'],
1325
+ });
1326
+
1327
+ // Disable all auto-instrumentation (manual APIs only)
1328
+ Senzor.init({
1329
+ apiKey: '...',
1330
+ instrumentations: false,
1331
+ });
1332
+ ```
1333
+
1334
+ ---
1335
+
1336
+ ## 19. Environment Variables
1337
+
1338
+ The preload entrypoint (`@senzops/apm-node/register`) reads:
1339
+
1340
+ | Variable | Maps To | Default |
1341
+ |----------|---------|---------|
1342
+ | `SENZOR_API_KEY` | `apiKey` | — |
1343
+ | `SENZOR_APM_API_KEY` | `apiKey` (alt) | — |
1344
+ | `SENZOR_SERVICE_API_KEY` | `apiKey` (alt) | — |
1345
+ | `SENZOR_ENDPOINT` | `endpoint` | `https://api.senzor.dev` |
1346
+ | `SENZOR_APM_ENDPOINT` | `endpoint` (alt) | — |
1347
+ | `SENZOR_DEBUG` | `debug` | `false` |
1348
+ | `SENZOR_AUTO_LOGS` | `autoLogs` | `true` |
1349
+ | `SENZOR_BATCH_SIZE` | `batchSize` | `100` (or `10` in Lambda) |
1350
+ | `SENZOR_FLUSH_INTERVAL` | `flushInterval` | `10000` (or `0` in Lambda) |
1351
+ | `SENZOR_FLUSH_TIMEOUT_MS` | `flushTimeoutMs` | `5000` |
1352
+ | `SENZOR_MAX_QUEUE_SIZE` | `maxQueueSize` | `10000` |
1353
+ | `SENZOR_MAX_SPANS_PER_TRACE` | `maxSpansPerTrace` | `500` |
1354
+ | `SENZOR_CAPTURE_HEADERS` | `captureHeaders` | `false` |
1355
+ | `SENZOR_CAPTURE_DB_STATEMENT` | `captureDbStatement` | `true` |
1356
+ | `SENZOR_FRAMEWORK_SPANS` | `frameworkSpans` | `true` |
1357
+ | `SENZOR_CAPTURE_MIDDLEWARE_SPANS` | `captureMiddlewareSpans` | `true` |
1358
+ | `SENZOR_CAPTURE_ROUTER_SPANS` | `captureRouterSpans` | `true` |
1359
+ | `SENZOR_CAPTURE_LIFECYCLE_HOOK_SPANS` | `captureLifecycleHookSpans` | `true` |
1360
+ | `SENZOR_RUNTIME_METRICS` | `runtimeMetrics` | `true` (auto `false` in Lambda) |
1361
+ | `SENZOR_RUNTIME_METRICS_INTERVAL` | `runtimeMetricsInterval` | `15000` |
1362
+
1363
+ Environment variable values always take precedence over programmatic configuration.
1364
+
1365
+ ---
1366
+
1367
+ ## 20. Security, Privacy & Cardinality
1368
+
1369
+ ### Automatic Redaction
1370
+
1371
+ These keys are automatically redacted from attributes, headers, logs, and error context:
1372
+
1373
+ `authorization`, `cookie`, `set-cookie`, `password`, `passwd`, `pwd`, `secret`, `token`, `api-key`, `x-api-key`, `access-token`, `refresh-token`, `client-secret`, `private-key`
1374
+
1375
+ ### Cardinality Controls
1376
+
1377
+ - URL routes are normalized: `/users/123` becomes `/users/:id`
1378
+ - UUIDs and MongoDB ObjectIDs are replaced with `:id` in route names
1379
+ - SQL statements are normalized to remove literal values
1380
+ - Attribute count and string length are bounded
1381
+ - Span count per trace is bounded
1382
+ - Queue size is bounded with oldest-first eviction
1383
+
1384
+ ### Recommendations
1385
+
1386
+ - Keep `captureHeaders` disabled unless required for debugging
1387
+ - Never capture request/response bodies
1388
+ - Use route templates (e.g., `/users/:id`) rather than actual paths in span names
1389
+ - Store IDs in metadata, not in operation names
1390
+ - Use `instrumentations: [...]` to disable instrumentations you don't need
1391
+
1392
+ ---
1393
+
1394
+ ## 21. Transport Behavior
1395
+
1396
+ The SDK batches telemetry and sends it asynchronously using two independent queues:
1397
+
1398
+ - **APM Queue**: traces, errors, logs, runtime metrics -> `/api/ingest/apm`
1399
+ - **Task Queue**: task runs, task errors, task logs -> `/api/ingest/task`
1400
+
1401
+ Behavior:
1402
+
1403
+ - Queues are bounded by `maxQueueSize` (oldest items evicted first)
1404
+ - Flush triggers on `batchSize` threshold or `flushInterval` timer
1405
+ - Each ingest request has a `flushTimeoutMs` timeout
1406
+ - Failed batches are requeued within queue limits (retry on next flush)
1407
+ - SDK ingest requests are marked as internal (not traced recursively)
1408
+ - A best-effort flush runs on `process.beforeExit`
1409
+ - In Lambda, flush is forced before handler return + Extensions API SHUTDOWN hook
1410
+
1411
+ ### Serverless Flush
1412
+
1413
+ Always call `await Senzor.flush()` before the function exits in serverless environments (unless using `wrapLambda` or `Senzor.worker()`, which handle this automatically).
1414
+
1415
+ ---
1416
+
1417
+ ## 22. Ingestion Payload Format
1418
+
1419
+ ### APM Ingest (`POST /api/ingest/apm`)
1420
+
1421
+ ```json
1422
+ {
1423
+ "traces": [{
1424
+ "traceId": "f3b2c2c9c70443f5a4b7f0ff6d5b9a17",
1425
+ "parentTraceId": "8cb1f2e2f43e4b2b8b3411c7df81e7e0",
1426
+ "parentSpanId": "6c62c908a21d4f49",
1427
+ "rootSpanId": "91f6c551d5a2403f",
1428
+ "method": "GET",
1429
+ "route": "/orders/:orderId",
1430
+ "path": "/orders/ord_123",
1431
+ "status": 200,
1432
+ "duration": 53.29,
1433
+ "ip": "203.0.113.10",
1434
+ "userAgent": "Mozilla/5.0",
1435
+ "timestamp": "2026-05-26T15:30:00.000Z",
1436
+ "spans": [{
1437
+ "spanId": "9d8a4d5f17e24d2a",
1438
+ "parentSpanId": "91f6c551d5a2403f",
1439
+ "name": "Postgres SELECT",
1440
+ "type": "db",
1441
+ "startTime": 5.41,
1442
+ "duration": 12.45,
1443
+ "status": 0,
1444
+ "meta": {
1445
+ "db.system.name": "postgresql",
1446
+ "db.operation.name": "SELECT",
1447
+ "db.query.text": "SELECT id FROM users WHERE id = $?"
1448
+ }
1449
+ }]
1450
+ }],
1451
+ "errors": [{
1452
+ "errorClass": "Error",
1453
+ "message": "Connection refused",
1454
+ "stackTrace": "Error: Connection refused\n at ...",
1455
+ "traceId": "f3b2c2c9c70443f5a4b7f0ff6d5b9a17",
1456
+ "context": { "operation": "db_connect" },
1457
+ "timestamp": "2026-05-26T15:30:00.000Z"
1458
+ }],
1459
+ "logs": [{
1460
+ "message": "order processed",
1461
+ "level": "info",
1462
+ "traceId": "f3b2c2c9c70443f5a4b7f0ff6d5b9a17",
1463
+ "attributes": { "orderId": "ord_123" },
1464
+ "timestamp": "2026-05-26T15:30:00.000Z"
1465
+ }],
1466
+ "runtimeMetrics": [{
1467
+ "timestamp": "2026-05-26T15:30:00.000Z",
1468
+ "eventLoop": { "lagP50": 1.2, "lagP99": 4.8, "lagMax": 12.1, "utilization": 0.34 },
1469
+ "gc": { "minor": 2.1, "major": 0, "incremental": 0.8 },
1470
+ "memory": { "heapUsed": 45000000, "heapTotal": 67000000, "rss": 89000000 }
1471
+ }]
1472
+ }
1473
+ ```
1474
+
1475
+ ### Task Ingest (`POST /api/ingest/task`)
1476
+
1477
+ ```json
1478
+ {
1479
+ "runs": [{
1480
+ "runId": "3917bd35-b1d6-4e23-a1d2-d969e1a7d6a1",
1481
+ "taskName": "billing:send_invoice_email",
1482
+ "taskType": "queue",
1483
+ "status": "success",
1484
+ "duration": 188.3,
1485
+ "queueDelay": 92,
1486
+ "attempts": 1,
1487
+ "triggerTraceId": "f3b2c2c9c70443f5a4b7f0ff6d5b9a17",
1488
+ "metadata": { "jobId": "142", "queueName": "billing" },
1489
+ "resourceMetrics": {
1490
+ "memoryDeltaBytes": 1048576,
1491
+ "cpuUserUs": 12000,
1492
+ "cpuSystemUs": 3000
1493
+ },
1494
+ "isDeadLetter": false,
1495
+ "spans": [],
1496
+ "timestamp": "2026-05-26T15:30:00.000Z"
1497
+ }],
1498
+ "errors": [],
1499
+ "logs": []
1500
+ }
1501
+ ```
1502
+
1503
+ ---
1504
+
1505
+ ## 23. Deployment Patterns
1506
+
1507
+ ### Docker
1508
+
1509
+ ```dockerfile
1510
+ FROM node:20-alpine
1511
+ WORKDIR /app
1512
+ COPY package*.json ./
1513
+ RUN npm ci --production
1514
+ COPY dist/ dist/
1515
+
1516
+ ENV SENZOR_API_KEY=sz_apm_xxx
1517
+ CMD ["node", "-r", "@senzops/apm-node/register", "dist/server.js"]
1518
+ ```
1519
+
1520
+ ### Kubernetes
1521
+
1522
+ ```yaml
1523
+ apiVersion: apps/v1
1524
+ kind: Deployment
1525
+ spec:
1526
+ template:
1527
+ spec:
1528
+ containers:
1529
+ - name: api
1530
+ image: my-api:latest
1531
+ env:
1532
+ - name: SENZOR_API_KEY
1533
+ valueFrom:
1534
+ secretKeyRef:
1535
+ name: senzor-secrets
1536
+ key: api-key
1537
+ - name: NODE_OPTIONS
1538
+ value: "-r @senzops/apm-node/register"
1539
+ ```
1540
+
1541
+ ### PM2
1542
+
1543
+ ```json
1544
+ {
1545
+ "apps": [{
1546
+ "name": "orders-api",
1547
+ "script": "dist/server.js",
1548
+ "node_args": "-r @senzops/apm-node/register",
1549
+ "env": {
1550
+ "SENZOR_API_KEY": "sz_apm_xxx",
1551
+ "NODE_ENV": "production"
1552
+ }
1553
+ }]
1554
+ }
1555
+ ```
1556
+
1557
+ ### AWS Lambda
1558
+
1559
+ See [Section 4: AWS Lambda Integration](#4-aws-lambda-integration).
1560
+
1561
+ ### Cloudflare Workers
1562
+
1563
+ See [Section 5: Cloudflare Workers Integration](#5-cloudflare-workers-integration).
1564
+
1565
+ ### Vercel (Serverless Functions)
1566
+
1567
+ ```ts
1568
+ // api/route.ts
1569
+ import Senzor from '@senzops/apm-node';
1570
+
1571
+ Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
1572
+
1573
+ export const GET = Senzor.wrapNextRoute(async (req) => {
1574
+ const result = await fetchData();
1575
+ return Response.json(result);
1576
+ });
1577
+ ```
1578
+
1579
+ ---
1580
+
1581
+ ## 24. Troubleshooting
1582
+
1583
+ ### Missing Spans
1584
+
1585
+ 1. **Check startup order.** Preload mode must load before application modules:
1586
+ ```sh
1587
+ node -r @senzops/apm-node/register server.js
1588
+ ```
1589
+ 2. **Programmatic mode:** Initialize `Senzor.init()` before importing `pg`, `mongodb`, `redis`, etc.
1590
+ 3. **Check if the library is supported** in the [instrumentation table](#6-auto-instrumentation-reference).
1591
+
1592
+ ### No Data in Senzor
1593
+
1594
+ 1. Verify `SENZOR_API_KEY` is set and valid.
1595
+ 2. Verify `endpoint` points to the correct Senzor environment.
1596
+ 3. Verify the runtime can reach the ingest endpoint (check firewall, VPC, DNS).
1597
+ 4. Enable `debug: true` to see flush attempts.
1598
+ 5. In serverless environments, ensure `Senzor.flush()` is called (or use `wrapLambda`/`worker()`).
1599
+
1600
+ ### Duplicate Traces
1601
+
1602
+ The SDK deduplicates active traces. If duplicates still appear:
1603
+ - Check whether both preload mode AND manual `track()` calls are active for the same request.
1604
+ - Ensure `Senzor.requestHandler()` is only attached once.
1605
+
1606
+ ### High Memory Usage
1607
+
1608
+ ```ts
1609
+ Senzor.init({
1610
+ apiKey: '...',
1611
+ maxQueueSize: 5000, // Reduce queue bounds
1612
+ maxSpansPerTrace: 250, // Reduce span retention
1613
+ maxAttributes: 32, // Reduce attribute count
1614
+ maxAttributeLength: 1024, // Reduce string lengths
1615
+ });
1616
+ ```
1617
+
1618
+ ### High-Cardinality Routes
1619
+
1620
+ Use framework wrappers that provide route templates:
1621
+
1622
+ ```ts
1623
+ app.use(Senzor.requestHandler()); // Captures /users/:id instead of /users/123
1624
+ ```
1625
+
1626
+ For manual traces, use normalized routes:
1627
+
1628
+ ```ts
1629
+ Senzor.track({ method: 'GET', route: '/users/:id', path: '/users/123', ... });
1630
+ ```
1631
+
1632
+ ### Lambda Cold Start Issues
1633
+
1634
+ - Ensure `Senzor.init()` or preload runs outside the handler (module scope).
1635
+ - Check that `NODE_OPTIONS` is set correctly for Lambda Layer deployments.
1636
+ - Use `Senzor.wrapLambda()` for proper cold start tagging.
1637
+
1638
+ ---
1639
+
1640
+ ## 25. Public API Reference
1641
+
1642
+ ```ts
1643
+ // Initialization
1644
+ Senzor.init(options: SenzorOptions) // Initialize SDK with API key
1645
+ Senzor.preload(options?: Partial<SenzorOptions>) // Pre-install hooks (used by register.ts)
1646
+
1647
+ // Telemetry
1648
+ Senzor.flush(): Promise<void> // Force flush all queued telemetry
1649
+ Senzor.track(data: object) // Send a manual trace
1650
+ Senzor.startSpan(name: string, type?: SpanType) // Start a manual child span
1651
+ Senzor.captureException(error: unknown, ctx?: object) // Capture an error
1652
+
1653
+ // Task Monitoring
1654
+ Senzor.wrapTask(name, type, options, fn) // Wrap function as monitored task
1655
+ Senzor.startTask(name, type, options, fn) // Start task monitoring context
1656
+
1657
+ // Framework Wrappers
1658
+ Senzor.requestHandler() // Express request middleware
1659
+ Senzor.errorHandler() // Express error middleware
1660
+ Senzor.fastifyPlugin // Fastify plugin
1661
+ Senzor.wrapNextRoute(handler) // Next.js App Router wrapper
1662
+ Senzor.wrapNextPages(handler) // Next.js Pages Router wrapper
1663
+ Senzor.wrapH3(handler) // H3/Nuxt/Nitro event handler wrapper
1664
+ Senzor.nitroPlugin // Nitro plugin for Cloudflare Workers
1665
+ Senzor.worker(handler) // Cloudflare Workers fetch handler wrapper
1666
+ Senzor.wrapLambda(handler) // AWS Lambda handler wrapper
1667
+ ```
1668
+
1669
+ ---
1670
+
1671
+ ## 26. Build & Publish
1672
+
1673
+ ```sh
1674
+ npm run build
1675
+ ```
1676
+
1677
+ Output:
1678
+
1679
+ | File | Format | Description |
1680
+ |------|--------|-------------|
1681
+ | `dist/index.js` | CommonJS | Main entry |
1682
+ | `dist/index.mjs` | ESM | Main entry |
1683
+ | `dist/index.global.js` | IIFE | Browser/CDN bundle |
1684
+ | `dist/register.js` | CommonJS | Preload entry |
1685
+ | `dist/register.mjs` | ESM | Preload entry |
1686
+ | `dist/index.d.ts` | TypeScript | Type declarations |
1687
+ | `dist/register.d.ts` | TypeScript | Type declarations |
1688
+
1689
+ Verify before publishing:
1690
+
1691
+ ```sh
1692
+ npx tsc --noEmit && npm run build
1693
+ ```