@senzops/apm-node 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.claude/worktrees/infallible-chatelet-f3fb36/.claude/settings.local.json +9 -0
  2. package/.claude/worktrees/infallible-chatelet-f3fb36/CHANGELOG.md +49 -0
  3. package/.claude/worktrees/infallible-chatelet-f3fb36/README.md +398 -0
  4. package/.claude/worktrees/infallible-chatelet-f3fb36/package-lock.json +1494 -0
  5. package/.claude/worktrees/infallible-chatelet-f3fb36/package.json +42 -0
  6. package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/client.ts +451 -0
  7. package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/context.ts +48 -0
  8. package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/normalizer.ts +44 -0
  9. package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/sanitizer.ts +203 -0
  10. package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/transport.ts +273 -0
  11. package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/types.ts +106 -0
  12. package/.claude/worktrees/infallible-chatelet-f3fb36/src/index.ts +36 -0
  13. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/bullmq.ts +195 -0
  14. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/cron.ts +204 -0
  15. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/express.ts +338 -0
  16. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/fastify.ts +296 -0
  17. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/framework.ts +301 -0
  18. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/hook.ts +134 -0
  19. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/http.ts +530 -0
  20. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/koa.ts +173 -0
  21. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/mongo.ts +202 -0
  22. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/mongoose.ts +156 -0
  23. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/mysql.ts +169 -0
  24. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/patch.ts +56 -0
  25. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/pg.ts +131 -0
  26. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/redis.ts +109 -0
  27. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/span.ts +73 -0
  28. package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/undici.ts +189 -0
  29. package/.claude/worktrees/infallible-chatelet-f3fb36/src/middleware/express.ts +48 -0
  30. package/.claude/worktrees/infallible-chatelet-f3fb36/src/register.ts +58 -0
  31. package/.claude/worktrees/infallible-chatelet-f3fb36/src/utils/getClientIp.ts +175 -0
  32. package/.claude/worktrees/infallible-chatelet-f3fb36/src/utils/ids.ts +7 -0
  33. package/.claude/worktrees/infallible-chatelet-f3fb36/src/utils/internal.ts +1 -0
  34. package/.claude/worktrees/infallible-chatelet-f3fb36/src/utils/sdkMeta.ts +6 -0
  35. package/.claude/worktrees/infallible-chatelet-f3fb36/src/utils/traceContext.ts +44 -0
  36. package/.claude/worktrees/infallible-chatelet-f3fb36/src/wrappers/fastify.ts +35 -0
  37. package/.claude/worktrees/infallible-chatelet-f3fb36/src/wrappers/h3.ts +59 -0
  38. package/.claude/worktrees/infallible-chatelet-f3fb36/src/wrappers/next.ts +131 -0
  39. package/.claude/worktrees/infallible-chatelet-f3fb36/tsconfig.json +15 -0
  40. package/.claude/worktrees/infallible-chatelet-f3fb36/tsup.config.ts +21 -0
  41. package/.claude/worktrees/infallible-chatelet-f3fb36/wiki.md +852 -0
  42. package/CHANGELOG.md +8 -0
  43. package/README.md +12 -0
  44. package/dist/index.d.mts +5 -0
  45. package/dist/index.d.ts +5 -0
  46. package/dist/index.global.js +1 -1
  47. package/dist/index.global.js.map +1 -1
  48. package/dist/index.js +1 -1
  49. package/dist/index.js.map +1 -1
  50. package/dist/index.mjs +1 -1
  51. package/dist/index.mjs.map +1 -1
  52. package/dist/register.js +1 -1
  53. package/dist/register.js.map +1 -1
  54. package/dist/register.mjs +1 -1
  55. package/dist/register.mjs.map +1 -1
  56. package/package.json +1 -1
  57. package/src/core/client.ts +8 -2
  58. package/src/core/types.ts +5 -0
  59. package/src/instrumentation/express.ts +338 -0
  60. package/src/instrumentation/fastify.ts +296 -0
  61. package/src/instrumentation/framework.ts +301 -0
  62. package/src/instrumentation/hook.ts +79 -192
  63. package/src/instrumentation/koa.ts +173 -0
  64. package/src/register.ts +16 -0
  65. package/src/wrappers/fastify.ts +10 -7
  66. package/src/wrappers/h3.ts +40 -16
  67. package/src/wrappers/next.ts +68 -21
  68. package/wiki.md +8 -0
@@ -0,0 +1,134 @@
1
+ import Module from 'module';
2
+
3
+ const SENZOR_PATCHED = Symbol.for('senzor.require.patched');
4
+ const SENZOR_HOOKS = Symbol.for('senzor.require.hooks');
5
+
6
+ type HookFn = (exports: unknown) => unknown | void;
7
+ type HookMap = Map<string, HookFn[]>;
8
+
9
+ // Module.createRequire works in both CJS and ESM contexts,
10
+ // unlike bare `require` which is unavailable in ESM builds.
11
+ const safeRequire: NodeRequire = Module.createRequire(
12
+ typeof __filename !== 'undefined'
13
+ ? __filename
14
+ : process.cwd() + '/'
15
+ );
16
+
17
+ function getHookRegistry(): HookMap {
18
+ const mod = Module as unknown as Record<symbol, HookMap>;
19
+
20
+ if (!mod[SENZOR_HOOKS]) {
21
+ Object.defineProperty(mod, SENZOR_HOOKS, {
22
+ value: new Map(),
23
+ enumerable: false
24
+ });
25
+ }
26
+
27
+ return mod[SENZOR_HOOKS];
28
+ }
29
+
30
+ function runHooks(moduleName: string, exports: unknown) {
31
+ const registry = (Module as unknown as Record<symbol, HookMap>)[SENZOR_HOOKS];
32
+ if (!registry) return exports;
33
+
34
+ const hooks = registry.get(moduleName);
35
+ if (!hooks?.length) return exports;
36
+
37
+ let currentExports = exports;
38
+
39
+ for (const hook of hooks) {
40
+ try {
41
+ const nextExports = hook(currentExports);
42
+ if (nextExports !== undefined) {
43
+ currentExports = nextExports;
44
+ }
45
+ } catch (err) {
46
+ console.error(`[Senzor] instrumentation failed for ${moduleName}`, err);
47
+ }
48
+ }
49
+
50
+ return currentExports;
51
+ }
52
+
53
+ function patchLoaderOnce() {
54
+ const mod = Module as unknown as any;
55
+
56
+ if (mod[SENZOR_PATCHED]) return;
57
+
58
+ const previousLoad = mod._load;
59
+
60
+ mod._load = function patchedLoad(
61
+ request: string,
62
+ parent: unknown,
63
+ isMain: boolean
64
+ ) {
65
+ const exports = previousLoad.apply(this, arguments);
66
+ return runHooks(request, exports);
67
+ };
68
+
69
+ Object.defineProperty(mod, SENZOR_PATCHED, {
70
+ value: true,
71
+ enumerable: false
72
+ });
73
+ }
74
+
75
+ function patchCached(moduleName: string, hook: HookFn) {
76
+ try {
77
+ const resolved = safeRequire.resolve(moduleName);
78
+ const cached = safeRequire.cache?.[resolved];
79
+
80
+ if (cached?.exports) {
81
+ const replacement = hook(cached.exports);
82
+ if (replacement !== undefined) {
83
+ cached.exports = replacement;
84
+ }
85
+ }
86
+ } catch { }
87
+ }
88
+
89
+ function tryRequire(moduleName: string, hook: HookFn) {
90
+ try {
91
+ const mod = safeRequire(moduleName);
92
+ if (mod) {
93
+ hook(mod);
94
+ }
95
+ } catch { }
96
+ }
97
+
98
+ function retryPatch(moduleName: string, hook: HookFn) {
99
+ let attempts = 0;
100
+ const max = 5;
101
+
102
+ const timer = setInterval(() => {
103
+ attempts++;
104
+
105
+ try {
106
+ const mod = safeRequire(moduleName);
107
+ if (mod) {
108
+ hook(mod);
109
+ clearInterval(timer);
110
+ }
111
+ } catch { }
112
+
113
+ if (attempts >= max) {
114
+ clearInterval(timer);
115
+ }
116
+ }, 200);
117
+
118
+ if (typeof timer.unref === 'function') timer.unref();
119
+ }
120
+
121
+ export const hookRequire = (moduleName: string, onRequire: HookFn) => {
122
+ const registry = getHookRegistry();
123
+
124
+ if (!registry.has(moduleName)) {
125
+ registry.set(moduleName, []);
126
+ }
127
+
128
+ registry.get(moduleName)!.push(onRequire);
129
+
130
+ patchLoaderOnce();
131
+ patchCached(moduleName, onRequire);
132
+ tryRequire(moduleName, onRequire);
133
+ retryPatch(moduleName, onRequire);
134
+ };
@@ -0,0 +1,530 @@
1
+ import http from 'http';
2
+ import https from 'https';
3
+ import { URL } from 'url';
4
+ import type { SenzorClient } from '../core/client';
5
+ import { Context } from '../core/context';
6
+ import { getRoute, normalizePath } from '../core/normalizer';
7
+ import { sanitizeHeaders } from '../core/sanitizer';
8
+ import { SenzorOptions } from '../core/types';
9
+ import { getClientIp } from '../utils/getClientIp';
10
+ import { SENZOR_INTERNAL_HEADER } from '../utils/internal';
11
+ import { generateTraceparent } from '../utils/traceContext';
12
+ import { patchMethod } from './patch';
13
+ import { runWithCapturedSpan, startCapturedSpan } from './span';
14
+
15
+ const getDebug = (options?: SenzorOptions): boolean =>
16
+ Boolean(options?.debug);
17
+
18
+ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
19
+ typeof value === 'object' &&
20
+ value !== null &&
21
+ !(value instanceof URL) &&
22
+ !(value instanceof Function) &&
23
+ !Array.isArray(value);
24
+
25
+ const headerValue = (
26
+ headers: unknown,
27
+ key: string
28
+ ): unknown => {
29
+ if (!headers) return undefined;
30
+
31
+ if (typeof Headers !== 'undefined' && headers instanceof Headers) {
32
+ return headers.get(key);
33
+ }
34
+
35
+ if (Array.isArray(headers)) {
36
+ const found = headers.find(
37
+ ([name]) => String(name).toLowerCase() === key.toLowerCase()
38
+ );
39
+ return found?.[1];
40
+ }
41
+
42
+ if (typeof headers === 'object') {
43
+ const normalizedKey = key.toLowerCase();
44
+ for (const [name, value] of Object.entries(headers)) {
45
+ if (name.toLowerCase() === normalizedKey) return value;
46
+ }
47
+ }
48
+
49
+ return undefined;
50
+ };
51
+
52
+ const hasInternalHeader = (headers: unknown): boolean =>
53
+ String(headerValue(headers, SENZOR_INTERNAL_HEADER) || '').toLowerCase() ===
54
+ 'true';
55
+
56
+ const shouldIgnoreUrl = (
57
+ urlString: string,
58
+ ingestUrl: string,
59
+ headers?: unknown
60
+ ): boolean => {
61
+ if (hasInternalHeader(headers)) return true;
62
+ if (!urlString) return false;
63
+
64
+ try {
65
+ const url = new URL(urlString);
66
+ const ingest = new URL(ingestUrl);
67
+ return (
68
+ url.hostname === ingest.hostname &&
69
+ url.pathname.startsWith('/api/ingest')
70
+ );
71
+ } catch {
72
+ return ingestUrl ? urlString.includes(ingestUrl) : false;
73
+ }
74
+ };
75
+
76
+ const cloneHeaders = (headers: unknown): Record<string, unknown> => {
77
+ if (!headers) return {};
78
+
79
+ if (typeof Headers !== 'undefined' && headers instanceof Headers) {
80
+ const cloned: Record<string, unknown> = {};
81
+ headers.forEach((value, key) => {
82
+ cloned[key] = value;
83
+ });
84
+ return cloned;
85
+ }
86
+
87
+ if (Array.isArray(headers)) {
88
+ return headers.reduce<Record<string, unknown>>((acc, [key, value]) => {
89
+ acc[key] = value;
90
+ return acc;
91
+ }, {});
92
+ }
93
+
94
+ if (typeof headers === 'object') {
95
+ return { ...(headers as Record<string, unknown>) };
96
+ }
97
+
98
+ return {};
99
+ };
100
+
101
+ const setHeader = (
102
+ headers: Record<string, unknown>,
103
+ key: string,
104
+ value: string
105
+ ) => {
106
+ const existingKey = Object.keys(headers).find(
107
+ (header) => header.toLowerCase() === key.toLowerCase()
108
+ );
109
+ headers[existingKey || key] = value;
110
+ };
111
+
112
+ interface PreparedRequest {
113
+ args: any[];
114
+ options: Record<string, any>;
115
+ url: string;
116
+ method: string;
117
+ hostname: string;
118
+ path: string;
119
+ }
120
+
121
+ const prepareRequestArgs = (
122
+ args: any[],
123
+ defaultProtocol: 'http:' | 'https:'
124
+ ): PreparedRequest => {
125
+ const nextArgs = [...args];
126
+ let optionsIndex = 0;
127
+ let options: Record<string, any> = {};
128
+ let urlFromArg: URL | null = null;
129
+
130
+ if (typeof nextArgs[0] === 'string' || nextArgs[0] instanceof URL) {
131
+ try {
132
+ urlFromArg = new URL(nextArgs[0].toString());
133
+ } catch {
134
+ urlFromArg = null;
135
+ }
136
+
137
+ if (isPlainObject(nextArgs[1])) {
138
+ optionsIndex = 1;
139
+ options = {
140
+ ...nextArgs[1],
141
+ headers: cloneHeaders(nextArgs[1].headers)
142
+ };
143
+ nextArgs[1] = options;
144
+ } else {
145
+ optionsIndex = 1;
146
+ options = { headers: {} };
147
+ nextArgs.splice(1, 0, options);
148
+ }
149
+ } else if (isPlainObject(nextArgs[0])) {
150
+ optionsIndex = 0;
151
+ options = {
152
+ ...nextArgs[0],
153
+ headers: cloneHeaders(nextArgs[0].headers)
154
+ };
155
+ nextArgs[0] = options;
156
+ } else {
157
+ optionsIndex = 0;
158
+ options = { headers: {} };
159
+ nextArgs[0] = options;
160
+ }
161
+
162
+ if (!options.headers) options.headers = {};
163
+ nextArgs[optionsIndex] = options;
164
+
165
+ const protocol =
166
+ options.protocol ||
167
+ urlFromArg?.protocol ||
168
+ (options.port === 443 ? 'https:' : defaultProtocol);
169
+ const hostname =
170
+ options.hostname ||
171
+ options.host ||
172
+ urlFromArg?.hostname ||
173
+ 'localhost';
174
+ const path =
175
+ options.path ||
176
+ `${urlFromArg?.pathname || '/'}${urlFromArg?.search || ''}`;
177
+ const url = urlFromArg
178
+ ? urlFromArg.toString()
179
+ : `${protocol}//${hostname}${path}`;
180
+ const method = String(options.method || 'GET').toUpperCase();
181
+
182
+ return {
183
+ args: nextArgs,
184
+ options,
185
+ url,
186
+ method,
187
+ hostname: String(hostname).replace(/:\d+$/, ''),
188
+ path
189
+ };
190
+ };
191
+
192
+ const resolveIncomingRoute = (
193
+ req: any,
194
+ res: any,
195
+ path: string
196
+ ): string => {
197
+ if (res?.statusCode === 404) return 'Not Found';
198
+
199
+ try {
200
+ return getRoute(req, path);
201
+ } catch {
202
+ return normalizePath(path);
203
+ }
204
+ };
205
+
206
+ const patchIncomingServer = (
207
+ proto: any,
208
+ protocol: 'http' | 'https',
209
+ client: SenzorClient,
210
+ options?: SenzorOptions
211
+ ) => {
212
+ patchMethod(
213
+ proto,
214
+ 'emit',
215
+ `senzor.${protocol}.server`,
216
+ (original) =>
217
+ function patchedEmit(this: any, event: string, ...args: any[]) {
218
+ if (event !== 'request') {
219
+ return original.call(this, event, ...args);
220
+ }
221
+
222
+ const req = args[0];
223
+ const res = args[1];
224
+
225
+ if (!req || !res || Context.current()?.contextType === 'apm') {
226
+ return original.call(this, event, ...args);
227
+ }
228
+
229
+ const rawPath = req.originalUrl || req.url || '/';
230
+ const path = String(rawPath).split('?')[0] || '/';
231
+ const headers = req.headers || {};
232
+
233
+ if (hasInternalHeader(headers)) {
234
+ return original.call(this, event, ...args);
235
+ }
236
+
237
+ return client.startTrace(
238
+ {
239
+ method: req.method || 'GET',
240
+ path: rawPath,
241
+ route: normalizePath(path),
242
+ ip: getClientIp(req),
243
+ userAgent: headers['user-agent'],
244
+ headers,
245
+ meta: {
246
+ protocol,
247
+ httpVersion: req.httpVersion,
248
+ headers: options?.captureHeaders
249
+ ? sanitizeHeaders(headers, options)
250
+ : undefined
251
+ }
252
+ },
253
+ () => {
254
+ const trace = Context.current();
255
+ let finalized = false;
256
+
257
+ const finalize = (reason: 'finish' | 'close' | 'error') => {
258
+ if (finalized || !trace) return;
259
+ finalized = true;
260
+
261
+ setImmediate(() => {
262
+ if (trace.ended) return;
263
+
264
+ Context.run(trace, () => {
265
+ client.endTrace(res.statusCode || 0, {
266
+ route: resolveIncomingRoute(req, res, path),
267
+ statusMessage: res.statusMessage,
268
+ meta: {
269
+ ...trace.data.meta,
270
+ endReason: reason
271
+ }
272
+ });
273
+ });
274
+ });
275
+ };
276
+
277
+ res.once('finish', () => finalize('finish'));
278
+ res.once('close', () => finalize('close'));
279
+ res.once('error', (error: Error) => {
280
+ client.captureError(error, {
281
+ instrumentation: `${protocol}.server`
282
+ });
283
+ finalize('error');
284
+ });
285
+
286
+ try {
287
+ return original.call(this, event, ...args);
288
+ } catch (error) {
289
+ client.captureError(error, {
290
+ instrumentation: `${protocol}.server`
291
+ });
292
+ finalize('error');
293
+ throw error;
294
+ }
295
+ }
296
+ );
297
+ }
298
+ );
299
+ };
300
+
301
+ const patchOutgoing = (
302
+ moduleRef: typeof http | typeof https,
303
+ protocol: 'http:' | 'https:',
304
+ ingestUrl: string,
305
+ options?: SenzorOptions
306
+ ) => {
307
+ const patchKeyPrefix = protocol === 'https:' ? 'senzor.https' : 'senzor.http';
308
+
309
+ const requestWrapper = (original: Function) =>
310
+ function patchedRequest(this: any, ...args: any[]) {
311
+ const prepared = prepareRequestArgs(args, protocol);
312
+
313
+ if (
314
+ shouldIgnoreUrl(
315
+ prepared.url,
316
+ ingestUrl,
317
+ prepared.options.headers
318
+ )
319
+ ) {
320
+ return original.apply(this, args);
321
+ }
322
+
323
+ const trace = Context.current();
324
+ if (!trace) return original.apply(this, args);
325
+
326
+ const span = startCapturedSpan(
327
+ `${prepared.method} ${prepared.hostname}`,
328
+ 'http',
329
+ {
330
+ url: prepared.url,
331
+ method: prepared.method,
332
+ library: protocol === 'https:' ? 'https' : 'http',
333
+ 'http.request.method': prepared.method,
334
+ 'url.full': prepared.url,
335
+ 'url.path': prepared.path,
336
+ 'server.address': prepared.hostname
337
+ },
338
+ options
339
+ );
340
+
341
+ if (span) {
342
+ setHeader(
343
+ prepared.options.headers,
344
+ 'traceparent',
345
+ generateTraceparent(trace.id, span.spanId)
346
+ );
347
+ setHeader(prepared.options.headers, 'x-senzor-trace-id', trace.id);
348
+ setHeader(
349
+ prepared.options.headers,
350
+ 'x-senzor-parent-span-id',
351
+ span.spanId
352
+ );
353
+ }
354
+
355
+ const invoke = () => {
356
+ const req = original.apply(this, prepared.args);
357
+ if (!span || !req || typeof req.once !== 'function') {
358
+ return req;
359
+ }
360
+
361
+ let completed = false;
362
+ const endSpan = (
363
+ status: number,
364
+ extraMeta: Record<string, unknown> = {}
365
+ ) => {
366
+ if (completed) return;
367
+ completed = true;
368
+ span.end(status, extraMeta);
369
+ };
370
+
371
+ req.once('response', (res: any) => {
372
+ const statusCode = res?.statusCode || 0;
373
+ const finish = () =>
374
+ endSpan(statusCode, {
375
+ 'http.response.status_code': statusCode
376
+ });
377
+
378
+ res.once('end', finish);
379
+ res.once('close', finish);
380
+ res.once('error', (error: Error) =>
381
+ endSpan(500, {
382
+ error: error.message,
383
+ 'error.type': error.name
384
+ })
385
+ );
386
+ });
387
+
388
+ req.once('timeout', () =>
389
+ endSpan(504, {
390
+ error: 'Request timed out',
391
+ 'error.type': 'TimeoutError'
392
+ })
393
+ );
394
+ req.once('error', (error: Error) =>
395
+ endSpan(500, {
396
+ error: error.message,
397
+ 'error.type': error.name
398
+ })
399
+ );
400
+
401
+ return req;
402
+ };
403
+
404
+ if (getDebug(options)) {
405
+ console.log(`[Senzor] Injecting trace headers to ${prepared.url}`);
406
+ }
407
+
408
+ return runWithCapturedSpan(span, invoke);
409
+ };
410
+
411
+ patchMethod(
412
+ moduleRef,
413
+ 'request',
414
+ `${patchKeyPrefix}.request`,
415
+ requestWrapper
416
+ );
417
+ patchMethod(
418
+ moduleRef,
419
+ 'get',
420
+ `${patchKeyPrefix}.get`,
421
+ requestWrapper
422
+ );
423
+ };
424
+
425
+ export const instrumentFetch = (
426
+ ingestUrl: string,
427
+ options?: SenzorOptions
428
+ ) => {
429
+ if (!globalThis.fetch) return;
430
+
431
+ patchMethod(
432
+ globalThis,
433
+ 'fetch',
434
+ 'senzor.fetch',
435
+ (original) =>
436
+ async function patchedFetch(
437
+ this: any,
438
+ input: any,
439
+ init?: any
440
+ ): Promise<Response> {
441
+ const urlString =
442
+ typeof input === 'string'
443
+ ? input
444
+ : input instanceof URL
445
+ ? input.toString()
446
+ : input?.url || '';
447
+
448
+ const originalHeaders = init?.headers || input?.headers;
449
+ if (shouldIgnoreUrl(urlString, ingestUrl, originalHeaders)) {
450
+ return original.call(this, input, init);
451
+ }
452
+
453
+ const trace = Context.current();
454
+ if (!trace) return original.call(this, input, init);
455
+
456
+ let hostname = 'unknown';
457
+ let path = '/';
458
+ try {
459
+ const url = new URL(urlString);
460
+ hostname = url.hostname;
461
+ path = `${url.pathname}${url.search}`;
462
+ } catch { }
463
+
464
+ const method = String(
465
+ init?.method || input?.method || 'GET'
466
+ ).toUpperCase();
467
+ const span = startCapturedSpan(
468
+ `${method} ${hostname}`,
469
+ 'http',
470
+ {
471
+ url: urlString,
472
+ method,
473
+ library: 'fetch',
474
+ 'http.request.method': method,
475
+ 'url.full': urlString,
476
+ 'url.path': path,
477
+ 'server.address': hostname
478
+ },
479
+ options
480
+ );
481
+
482
+ if (!span) return original.call(this, input, init);
483
+
484
+ const nextInit = { ...(init || {}) };
485
+ const headers =
486
+ typeof Headers !== 'undefined'
487
+ ? new Headers(originalHeaders || undefined)
488
+ : cloneHeaders(originalHeaders);
489
+
490
+ if (typeof Headers !== 'undefined' && headers instanceof Headers) {
491
+ headers.set('traceparent', generateTraceparent(trace.id, span.spanId));
492
+ headers.set('x-senzor-trace-id', trace.id);
493
+ headers.set('x-senzor-parent-span-id', span.spanId);
494
+ } else {
495
+ setHeader(headers as Record<string, unknown>, 'traceparent', generateTraceparent(trace.id, span.spanId));
496
+ setHeader(headers as Record<string, unknown>, 'x-senzor-trace-id', trace.id);
497
+ setHeader(headers as Record<string, unknown>, 'x-senzor-parent-span-id', span.spanId);
498
+ }
499
+ nextInit.headers = headers;
500
+
501
+ return runWithCapturedSpan(span, async () => {
502
+ try {
503
+ const response = await original.call(this, input, nextInit);
504
+ span.end(response.status, {
505
+ 'http.response.status_code': response.status
506
+ });
507
+ return response;
508
+ } catch (error: any) {
509
+ span.end(500, {
510
+ error: error?.message,
511
+ 'error.type': error?.name || 'Error'
512
+ });
513
+ throw error;
514
+ }
515
+ });
516
+ }
517
+ );
518
+ };
519
+
520
+ export const instrumentHttp = (
521
+ client: SenzorClient,
522
+ ingestUrl: string,
523
+ options?: SenzorOptions
524
+ ) => {
525
+ patchIncomingServer(http.Server?.prototype, 'http', client, options);
526
+ patchIncomingServer(https.Server?.prototype, 'https', client, options);
527
+
528
+ patchOutgoing(http, 'http:', ingestUrl, options);
529
+ patchOutgoing(https, 'https:', ingestUrl, options);
530
+ };