@senzops/apm-node 1.3.1 → 1.3.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/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/lambda-handler.js +1 -1
- package/dist/lambda-handler.js.map +1 -1
- package/dist/lambda-handler.mjs +1 -1
- package/dist/lambda-handler.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/core/client.ts +40 -11
- package/src/core/context.ts +26 -19
- package/src/core/transport.ts +25 -7
- package/src/instrumentation/amqplib.ts +1 -1
- package/src/instrumentation/hook.ts +120 -138
- package/src/instrumentation/undici.ts +15 -0
package/package.json
CHANGED
package/src/core/client.ts
CHANGED
|
@@ -9,14 +9,20 @@ import { sanitizeAttributes } from './sanitizer';
|
|
|
9
9
|
import { startCapturedSpan } from '../instrumentation/span';
|
|
10
10
|
import { RuntimeMetricsCollector } from '../instrumentation/runtime';
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const MAX_STRINGIFY_LENGTH = 8192;
|
|
13
|
+
|
|
14
14
|
const safeStringify = (obj: any): string => {
|
|
15
|
-
const
|
|
16
|
-
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
let length = 0;
|
|
17
|
+
return JSON.stringify(obj, function (_key, value) {
|
|
18
|
+
if (length > MAX_STRINGIFY_LENGTH) return undefined;
|
|
19
|
+
if (typeof value === 'string') {
|
|
20
|
+
length += value.length;
|
|
21
|
+
if (value.length > 2048) return value.slice(0, 2048) + '...[truncated]';
|
|
22
|
+
}
|
|
17
23
|
if (typeof value === 'object' && value !== null) {
|
|
18
|
-
if (
|
|
19
|
-
|
|
24
|
+
if (seen.has(value)) return '[Circular]';
|
|
25
|
+
seen.add(value);
|
|
20
26
|
}
|
|
21
27
|
return value;
|
|
22
28
|
});
|
|
@@ -299,14 +305,37 @@ export class SenzorClient {
|
|
|
299
305
|
}
|
|
300
306
|
};
|
|
301
307
|
|
|
308
|
+
const flushAndExit = (code: number) => {
|
|
309
|
+
if (this.transport) {
|
|
310
|
+
const timeout = setTimeout(() => process.exit(code), 2000);
|
|
311
|
+
if (typeof timeout.unref === 'function') timeout.unref();
|
|
312
|
+
this.transport.flush().then(
|
|
313
|
+
() => process.exit(code),
|
|
314
|
+
() => process.exit(code)
|
|
315
|
+
);
|
|
316
|
+
} else {
|
|
317
|
+
process.exit(code);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// Monitor-only: captures for telemetry without altering default crash behavior.
|
|
322
|
+
// Node.js will still print the stack and exit after this listener runs.
|
|
302
323
|
process.on('uncaughtExceptionMonitor', (error) => safeCapture(error, { type: 'uncaughtExceptionMonitor', severity: 'fatal' }));
|
|
303
|
-
|
|
324
|
+
|
|
304
325
|
process.on('unhandledRejection', (reason) => safeCapture(reason, { type: 'unhandledRejection', severity: 'error' }));
|
|
305
326
|
process.on('warning', (warning) => safeCapture(warning, { type: 'processWarning', severity: 'warning' }));
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
327
|
+
|
|
328
|
+
let shuttingDown = false;
|
|
329
|
+
|
|
330
|
+
const gracefulShutdown = (signal: string, exitCode: number) => {
|
|
331
|
+
if (shuttingDown) return;
|
|
332
|
+
shuttingDown = true;
|
|
333
|
+
safeCapture(new Error(`Process received ${signal}`), { type: 'processSignal', signal, severity: 'warning' });
|
|
334
|
+
flushAndExit(exitCode);
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM', 143));
|
|
338
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT', 130));
|
|
310
339
|
}
|
|
311
340
|
|
|
312
341
|
public startTrace<T>(data: Partial<ActiveTrace['data']> & { headers?: any }, next: () => T): T {
|
package/src/core/context.ts
CHANGED
|
@@ -6,54 +6,50 @@ interface IStorage<T> {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Per-callback context tracking for runtimes without AsyncLocalStorage.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Uses a stack instead of a single variable so overlapping synchronous
|
|
12
|
+
* Context.run() calls (e.g. nested middleware) don't clobber each other.
|
|
13
|
+
* Async context is propagated by chaining onto the returned thenable.
|
|
14
|
+
*
|
|
15
|
+
* NOT safe for truly concurrent requests in a single isolate — use
|
|
16
|
+
* AsyncLocalStorage for that. This exists as a last-resort fallback.
|
|
14
17
|
*/
|
|
15
18
|
class NaiveStorage<T> implements IStorage<T> {
|
|
16
|
-
private
|
|
19
|
+
private stack: T[] = [];
|
|
17
20
|
|
|
18
21
|
run<R>(store: T, callback: (...args: any[]) => R, ...args: any[]): R {
|
|
19
|
-
|
|
20
|
-
this.store = store;
|
|
22
|
+
this.stack.push(store);
|
|
21
23
|
|
|
22
24
|
let result: R;
|
|
23
25
|
try {
|
|
24
26
|
result = callback(...args);
|
|
25
27
|
} catch (err) {
|
|
26
|
-
this.
|
|
28
|
+
this.stack.pop();
|
|
27
29
|
throw err;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
// If the callback returned a thenable (async handler), defer the restore
|
|
31
|
-
// until the promise settles so Context.current() works across awaits.
|
|
32
32
|
if (result != null && typeof (result as any).then === 'function') {
|
|
33
33
|
const promise = (result as any).then(
|
|
34
|
-
(val: any) => { this.
|
|
35
|
-
(err: any) => { this.
|
|
34
|
+
(val: any) => { this.stack.pop(); return val; },
|
|
35
|
+
(err: any) => { this.stack.pop(); throw err; }
|
|
36
36
|
);
|
|
37
37
|
return promise as R;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
this.store = prev;
|
|
40
|
+
this.stack.pop();
|
|
42
41
|
return result;
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
getStore(): T | undefined {
|
|
46
|
-
return this.
|
|
45
|
+
return this.stack.length > 0 ? this.stack[this.stack.length - 1] : undefined;
|
|
47
46
|
}
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
/**
|
|
51
50
|
* Resolve the best available async context storage.
|
|
52
51
|
*
|
|
53
|
-
*
|
|
54
|
-
* falls back to NaiveStorage, subsequent calls to resolveStorage() will retry.
|
|
55
|
-
* This handles runtimes where AsyncLocalStorage becomes available after module
|
|
56
|
-
* init (e.g. some Workers configurations).
|
|
52
|
+
* Tries multiple resolution strategies to cover CJS, ESM, and edge runtimes.
|
|
57
53
|
*/
|
|
58
54
|
const tryResolveALS = <T>(): IStorage<T> | null => {
|
|
59
55
|
// 1. Check globalThis (Cloudflare Workers nodejs_compat_v2, Bun, Deno)
|
|
@@ -76,6 +72,17 @@ const tryResolveALS = <T>(): IStorage<T> | null => {
|
|
|
76
72
|
}
|
|
77
73
|
} catch {}
|
|
78
74
|
|
|
75
|
+
// 3. ESM fallback: indirect require via Function constructor
|
|
76
|
+
// In bundled ESM (tsup/esbuild), `require` is shimmed and steps 1-2 work.
|
|
77
|
+
// This step handles raw ESM where require is truly unavailable.
|
|
78
|
+
try {
|
|
79
|
+
if (typeof require === 'undefined' && typeof process !== 'undefined') {
|
|
80
|
+
const fn = new Function('try { var m = require("module"); return m.createRequire(process.cwd() + "/")("node:async_hooks"); } catch(e) { return null; }');
|
|
81
|
+
const mod = fn();
|
|
82
|
+
if (mod?.AsyncLocalStorage) return new mod.AsyncLocalStorage();
|
|
83
|
+
}
|
|
84
|
+
} catch {}
|
|
85
|
+
|
|
79
86
|
return null;
|
|
80
87
|
};
|
|
81
88
|
|
package/src/core/transport.ts
CHANGED
|
@@ -15,6 +15,9 @@ interface TaskPayload {
|
|
|
15
15
|
logs: SenzorLog[];
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const MAX_BACKOFF_MS = 60_000;
|
|
19
|
+
const BASE_BACKOFF_MS = 1_000;
|
|
20
|
+
|
|
18
21
|
export class Transport {
|
|
19
22
|
private traceQueue: Trace[] = [];
|
|
20
23
|
private apmErrorQueue: SenzorError[] = [];
|
|
@@ -33,6 +36,9 @@ export class Transport {
|
|
|
33
36
|
private flushAgain = false;
|
|
34
37
|
private droppedItems = 0;
|
|
35
38
|
|
|
39
|
+
private consecutiveFailures = 0;
|
|
40
|
+
private backoffUntil = 0;
|
|
41
|
+
|
|
36
42
|
constructor(private config: SenzorOptions) {
|
|
37
43
|
const baseEndpoint = config.endpoint || 'https://api.senzor.dev';
|
|
38
44
|
this.apmEndpoint = baseEndpoint.includes('/api/ingest')
|
|
@@ -41,10 +47,6 @@ export class Transport {
|
|
|
41
47
|
this.taskEndpoint = baseEndpoint.includes('/api/ingest')
|
|
42
48
|
? baseEndpoint.replace('/apm', '/task')
|
|
43
49
|
: `${baseEndpoint}/api/ingest/task`;
|
|
44
|
-
|
|
45
|
-
// Timer and shutdown flush are deferred to first enqueue.
|
|
46
|
-
// Cloudflare Workers forbids setInterval / process access in global scope,
|
|
47
|
-
// and init() may be called at module evaluation time (e.g. Nitro plugins).
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
private ensureTimer() {
|
|
@@ -94,7 +96,6 @@ export class Transport {
|
|
|
94
96
|
|
|
95
97
|
public addRuntimeMetrics(payload: RuntimeMetricsPayload) {
|
|
96
98
|
this.enqueue(this.runtimeMetricsQueue, payload);
|
|
97
|
-
// Runtime metrics don't trigger immediate flush — they ride the next timer
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
private enqueue<T>(queue: T[], item: T) {
|
|
@@ -236,6 +237,8 @@ export class Transport {
|
|
|
236
237
|
return;
|
|
237
238
|
}
|
|
238
239
|
|
|
240
|
+
if (Date.now() < this.backoffUntil) return;
|
|
241
|
+
|
|
239
242
|
this.isFlushing = true;
|
|
240
243
|
|
|
241
244
|
try {
|
|
@@ -271,6 +274,21 @@ export class Transport {
|
|
|
271
274
|
(result) => result.status === 'rejected'
|
|
272
275
|
);
|
|
273
276
|
|
|
277
|
+
if (failures.length > 0) {
|
|
278
|
+
this.consecutiveFailures++;
|
|
279
|
+
const delay = Math.min(
|
|
280
|
+
BASE_BACKOFF_MS * Math.pow(2, this.consecutiveFailures - 1),
|
|
281
|
+
MAX_BACKOFF_MS
|
|
282
|
+
);
|
|
283
|
+
this.backoffUntil = Date.now() + delay;
|
|
284
|
+
if (this.config.debug) {
|
|
285
|
+
console.warn(`[Senzor] Flush failed (attempt ${this.consecutiveFailures}), backing off ${delay}ms`);
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
this.consecutiveFailures = 0;
|
|
289
|
+
this.backoffUntil = 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
274
292
|
if (this.config.debug) {
|
|
275
293
|
console.log(
|
|
276
294
|
`[Senzor] Flushed: APM(${apmPayload.traces.length} traces, ${apmPayload.logs.length} logs), Task(${taskPayload.runs.length} runs, ${taskPayload.logs.length} logs), failures=${failures.length}, dropped=${this.droppedItems}`
|
|
@@ -296,10 +314,10 @@ export class Transport {
|
|
|
296
314
|
enumerable: false
|
|
297
315
|
});
|
|
298
316
|
|
|
299
|
-
const
|
|
317
|
+
const flushBestEffort = () => {
|
|
300
318
|
void this.flush();
|
|
301
319
|
};
|
|
302
320
|
|
|
303
|
-
process.once('beforeExit',
|
|
321
|
+
process.once('beforeExit', flushBestEffort);
|
|
304
322
|
}
|
|
305
323
|
}
|
|
@@ -194,7 +194,7 @@ const patchChannel = (channelProto: any, options?: SenzorOptions) => {
|
|
|
194
194
|
function patchedConsume(
|
|
195
195
|
this: any,
|
|
196
196
|
queue: string,
|
|
197
|
-
callback: (msg: any) =>
|
|
197
|
+
callback: (msg: any) => any,
|
|
198
198
|
consumeOptions?: any
|
|
199
199
|
) {
|
|
200
200
|
if (typeof callback !== 'function') {
|
|
@@ -1,138 +1,120 @@
|
|
|
1
|
-
type HookFn = (exports: unknown) => unknown | void;
|
|
2
|
-
type HookMap = Map<string, HookFn[]>;
|
|
3
|
-
|
|
4
|
-
let Module: any;
|
|
5
|
-
let safeRequire: NodeRequire;
|
|
6
|
-
|
|
7
|
-
try {
|
|
8
|
-
Module = require('module');
|
|
9
|
-
safeRequire = Module.createRequire(
|
|
10
|
-
typeof __filename !== 'undefined'
|
|
11
|
-
? __filename
|
|
12
|
-
: process.cwd() + '/'
|
|
13
|
-
);
|
|
14
|
-
} catch {}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (!
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export const hookRequire = (moduleName: string, onRequire: HookFn) => {
|
|
124
|
-
if (!Module || !safeRequire) return;
|
|
125
|
-
|
|
126
|
-
const registry = getHookRegistry();
|
|
127
|
-
|
|
128
|
-
if (!registry.has(moduleName)) {
|
|
129
|
-
registry.set(moduleName, []);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
registry.get(moduleName)!.push(onRequire);
|
|
133
|
-
|
|
134
|
-
patchLoaderOnce();
|
|
135
|
-
patchCached(moduleName, onRequire);
|
|
136
|
-
tryRequire(moduleName, onRequire);
|
|
137
|
-
retryPatch(moduleName, onRequire);
|
|
138
|
-
};
|
|
1
|
+
type HookFn = (exports: unknown) => unknown | void;
|
|
2
|
+
type HookMap = Map<string, HookFn[]>;
|
|
3
|
+
|
|
4
|
+
let Module: any = null;
|
|
5
|
+
let safeRequire: NodeRequire | null = null;
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
Module = require('module');
|
|
9
|
+
safeRequire = Module.createRequire(
|
|
10
|
+
typeof __filename !== 'undefined'
|
|
11
|
+
? __filename
|
|
12
|
+
: process.cwd() + '/'
|
|
13
|
+
);
|
|
14
|
+
} catch {}
|
|
15
|
+
|
|
16
|
+
// ESM fallback: if native require is unavailable, try to bootstrap via createRequire
|
|
17
|
+
if (!Module || !safeRequire) {
|
|
18
|
+
try {
|
|
19
|
+
const nodeModule = (Function('try { return require("module") } catch(e) { return null }'))();
|
|
20
|
+
if (nodeModule?.createRequire) {
|
|
21
|
+
Module = nodeModule;
|
|
22
|
+
const cwd = typeof process !== 'undefined' && process.cwd ? process.cwd() : '/';
|
|
23
|
+
safeRequire = nodeModule.createRequire(cwd + '/');
|
|
24
|
+
}
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const SENZOR_PATCHED = Symbol.for('senzor.require.patched');
|
|
29
|
+
const SENZOR_HOOKS = Symbol.for('senzor.require.hooks');
|
|
30
|
+
|
|
31
|
+
function getHookRegistry(): HookMap {
|
|
32
|
+
const mod = Module as unknown as Record<symbol, HookMap>;
|
|
33
|
+
|
|
34
|
+
if (!mod[SENZOR_HOOKS]) {
|
|
35
|
+
Object.defineProperty(mod, SENZOR_HOOKS, {
|
|
36
|
+
value: new Map(),
|
|
37
|
+
enumerable: false
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return mod[SENZOR_HOOKS];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function runHooks(moduleName: string, exports: unknown) {
|
|
45
|
+
const registry = (Module as unknown as Record<symbol, HookMap>)[SENZOR_HOOKS];
|
|
46
|
+
if (!registry) return exports;
|
|
47
|
+
|
|
48
|
+
const hooks = registry.get(moduleName);
|
|
49
|
+
if (!hooks?.length) return exports;
|
|
50
|
+
|
|
51
|
+
let currentExports = exports;
|
|
52
|
+
|
|
53
|
+
for (const hook of hooks) {
|
|
54
|
+
try {
|
|
55
|
+
const nextExports = hook(currentExports);
|
|
56
|
+
if (nextExports !== undefined) {
|
|
57
|
+
currentExports = nextExports;
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error(`[Senzor] instrumentation failed for ${moduleName}`, err);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return currentExports;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function patchLoaderOnce() {
|
|
68
|
+
const mod = Module as unknown as any;
|
|
69
|
+
|
|
70
|
+
if (mod[SENZOR_PATCHED]) return;
|
|
71
|
+
|
|
72
|
+
// Module._load is CJS-specific; in pure ESM runtimes it may not exist
|
|
73
|
+
if (typeof mod._load !== 'function') return;
|
|
74
|
+
|
|
75
|
+
const previousLoad = mod._load;
|
|
76
|
+
|
|
77
|
+
mod._load = function patchedLoad(
|
|
78
|
+
request: string,
|
|
79
|
+
parent: unknown,
|
|
80
|
+
isMain: boolean
|
|
81
|
+
) {
|
|
82
|
+
const exports = previousLoad.apply(this, arguments);
|
|
83
|
+
return runHooks(request, exports);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
Object.defineProperty(mod, SENZOR_PATCHED, {
|
|
87
|
+
value: true,
|
|
88
|
+
enumerable: false
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function patchCached(moduleName: string, hook: HookFn) {
|
|
93
|
+
if (!safeRequire) return;
|
|
94
|
+
try {
|
|
95
|
+
const resolved = safeRequire.resolve(moduleName);
|
|
96
|
+
const cached = safeRequire.cache?.[resolved];
|
|
97
|
+
|
|
98
|
+
if (cached?.exports) {
|
|
99
|
+
const replacement = hook(cached.exports);
|
|
100
|
+
if (replacement !== undefined) {
|
|
101
|
+
cached.exports = replacement;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch { }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const hookRequire = (moduleName: string, onRequire: HookFn) => {
|
|
108
|
+
if (!Module || !safeRequire) return;
|
|
109
|
+
|
|
110
|
+
const registry = getHookRegistry();
|
|
111
|
+
|
|
112
|
+
if (!registry.has(moduleName)) {
|
|
113
|
+
registry.set(moduleName, []);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
registry.get(moduleName)!.push(onRequire);
|
|
117
|
+
|
|
118
|
+
patchLoaderOnce();
|
|
119
|
+
patchCached(moduleName, onRequire);
|
|
120
|
+
};
|
|
@@ -70,6 +70,21 @@ const patchRequestLike = (
|
|
|
70
70
|
return original.apply(this, arguments as any);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// Skip if the request already has Senzor trace headers injected by
|
|
74
|
+
// the fetch instrumentation (Node.js fetch delegates to undici internally).
|
|
75
|
+
const existingHeaders = opts?.headers;
|
|
76
|
+
if (existingHeaders) {
|
|
77
|
+
let hasTrace = false;
|
|
78
|
+
if (typeof Headers !== 'undefined' && existingHeaders instanceof Headers) {
|
|
79
|
+
hasTrace = existingHeaders.has('x-senzor-trace-id');
|
|
80
|
+
} else if (Array.isArray(existingHeaders)) {
|
|
81
|
+
hasTrace = existingHeaders.some(([k]: [string, string]) => String(k).toLowerCase() === 'x-senzor-trace-id');
|
|
82
|
+
} else if (typeof existingHeaders === 'object') {
|
|
83
|
+
hasTrace = Object.keys(existingHeaders).some(k => k.toLowerCase() === 'x-senzor-trace-id');
|
|
84
|
+
}
|
|
85
|
+
if (hasTrace) return original.apply(this, arguments as any);
|
|
86
|
+
}
|
|
87
|
+
|
|
73
88
|
const details = getUrlDetails(input?.origin ? input.origin : input);
|
|
74
89
|
const method = String(opts?.method || 'GET').toUpperCase();
|
|
75
90
|
const span = startCapturedSpan(
|