@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.
- package/.claude/worktrees/infallible-chatelet-f3fb36/.claude/settings.local.json +9 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/CHANGELOG.md +49 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/README.md +398 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/package-lock.json +1494 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/package.json +42 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/client.ts +451 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/context.ts +48 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/normalizer.ts +44 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/sanitizer.ts +203 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/transport.ts +273 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/types.ts +106 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/index.ts +36 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/bullmq.ts +195 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/cron.ts +204 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/express.ts +338 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/fastify.ts +296 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/framework.ts +301 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/hook.ts +134 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/http.ts +530 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/koa.ts +173 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/mongo.ts +202 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/mongoose.ts +156 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/mysql.ts +169 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/patch.ts +56 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/pg.ts +131 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/redis.ts +109 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/span.ts +73 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/undici.ts +189 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/middleware/express.ts +48 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/register.ts +58 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/utils/getClientIp.ts +175 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/utils/ids.ts +7 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/utils/internal.ts +1 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/utils/sdkMeta.ts +6 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/utils/traceContext.ts +44 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/wrappers/fastify.ts +35 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/wrappers/h3.ts +59 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/wrappers/next.ts +131 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/tsconfig.json +15 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/tsup.config.ts +21 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/wiki.md +852 -0
- package/CHANGELOG.md +4 -0
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/register.js +1 -1
- package/dist/register.js.map +1 -1
- package/dist/register.mjs +1 -1
- package/dist/register.mjs.map +1 -1
- package/package.json +1 -1
- 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
|
+
```
|