@reproapp/node-sdk 0.0.1 → 0.0.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/docs/tracing.md +158 -0
- package/package.json +2 -13
- package/src/index.ts +2851 -0
- package/src/integrations/sendgrid.ts +184 -0
- package/tracer/cjs-hook.js +281 -0
- package/tracer/dep-hook.js +142 -0
- package/tracer/esm-loader.mjs +46 -0
- package/tracer/index.js +68 -0
- package/tracer/register.js +194 -0
- package/tracer/runtime.js +963 -0
- package/tracer/server.js +65 -0
- package/tracer/wrap-plugin.js +608 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,963 @@
|
|
|
1
|
+
// runtime.js
|
|
2
|
+
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
3
|
+
|
|
4
|
+
const als = new AsyncLocalStorage(); // { traceId, depth }
|
|
5
|
+
const listeners = new Set();
|
|
6
|
+
let EMITTING = false;
|
|
7
|
+
const quietEnv = process.env.TRACE_QUIET === '1';
|
|
8
|
+
// Off by default; set TRACE_DEBUG_UNAWAITED=1 to log unawaited enter/exit debug noise.
|
|
9
|
+
const DEBUG_UNAWAITED = process.env.TRACE_DEBUG_UNAWAITED === '1';
|
|
10
|
+
|
|
11
|
+
let functionLogsEnabled = !quietEnv;
|
|
12
|
+
let SPAN_COUNTER = 0;
|
|
13
|
+
|
|
14
|
+
// ---- console patch: trace console.* as top-level calls (safe; no recursion) ----
|
|
15
|
+
let CONSOLE_PATCHED = false;
|
|
16
|
+
function patchConsole() {
|
|
17
|
+
if (CONSOLE_PATCHED) return; CONSOLE_PATCHED = true;
|
|
18
|
+
|
|
19
|
+
const orig = {};
|
|
20
|
+
for (const m of ['log', 'info', 'warn', 'error', 'debug', 'trace']) {
|
|
21
|
+
if (typeof console[m] !== 'function') continue;
|
|
22
|
+
orig[m] = console[m];
|
|
23
|
+
console[m] = function tracedConsoleMethod(...args) {
|
|
24
|
+
// mark as core so it's obvious in logs
|
|
25
|
+
trace.enter(`console.${m}`, { file: 'node:console', line: null });
|
|
26
|
+
try {
|
|
27
|
+
return orig[m].apply(this, args);
|
|
28
|
+
} finally {
|
|
29
|
+
trace.exit({ fn: `console.${m}`, file: 'node:console', line: null });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isThenable(value) {
|
|
36
|
+
return value != null && typeof value.then === 'function';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isNativeFunction(fn) {
|
|
40
|
+
if (!fn || typeof fn !== 'function') return false;
|
|
41
|
+
try { return /\{\s*\[native code\]\s*\}/.test(Function.prototype.toString.call(fn)); }
|
|
42
|
+
catch { return false; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isFunctionInstrumented(fn) {
|
|
46
|
+
if (!fn || typeof fn !== 'function') return false;
|
|
47
|
+
const flag = '__repro_instrumented';
|
|
48
|
+
try {
|
|
49
|
+
if (Object.prototype.hasOwnProperty.call(fn, flag)) return !!fn[flag];
|
|
50
|
+
} catch {}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isMongooseQuery(value) {
|
|
55
|
+
if (!value) return false;
|
|
56
|
+
const hasExec = typeof value.exec === 'function';
|
|
57
|
+
const hasModel = !!value.model;
|
|
58
|
+
const ctorName = value?.constructor?.name;
|
|
59
|
+
const marked = value.__repro_is_query === true;
|
|
60
|
+
return (
|
|
61
|
+
isThenable(value) &&
|
|
62
|
+
(hasExec || marked || ctorName === 'Query' || hasModel)
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isQueryAlreadyExecuted(q) {
|
|
67
|
+
try {
|
|
68
|
+
return !!(q && (q._executionStack || q._executed));
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function queueQueryFinalizer(query, fn) {
|
|
75
|
+
try {
|
|
76
|
+
const list = query.__repro_query_finalizers || (query.__repro_query_finalizers = []);
|
|
77
|
+
list.push(fn);
|
|
78
|
+
return true;
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function pushSpan(ctx, depth, explicitParentId = null) {
|
|
85
|
+
const stack = ctx.__repro_span_stack || (ctx.__repro_span_stack = []);
|
|
86
|
+
const parent = explicitParentId !== null && explicitParentId !== undefined
|
|
87
|
+
? { id: explicitParentId, parentId: null }
|
|
88
|
+
: (stack.length ? stack[stack.length - 1] : null);
|
|
89
|
+
const span = { id: ++SPAN_COUNTER, parentId: parent ? parent.id : null, depth, file: null, line: null, sourceFile: null };
|
|
90
|
+
stack.push(span);
|
|
91
|
+
return span;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function popSpan(ctx) {
|
|
95
|
+
const stack = ctx.__repro_span_stack;
|
|
96
|
+
if (!Array.isArray(stack) || !stack.length) return { id: null, parentId: null, depth: null, file: null, line: null, sourceFile: null };
|
|
97
|
+
return stack.pop() || { id: null, parentId: null, depth: null, file: null, line: null, sourceFile: null };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const trace = {
|
|
101
|
+
on(fn){ listeners.add(fn); return () => listeners.delete(fn); },
|
|
102
|
+
withTrace(id, fn, depth = 0){ return als.run({ traceId: id, depth }, fn); },
|
|
103
|
+
enter(fn, meta, detail){
|
|
104
|
+
const parentSpanIdOverride = meta && Object.prototype.hasOwnProperty.call(meta, 'parentSpanId')
|
|
105
|
+
? meta.parentSpanId
|
|
106
|
+
: null;
|
|
107
|
+
const ctx = als.getStore() || {};
|
|
108
|
+
|
|
109
|
+
ctx.depth = (ctx.depth || 0) + 1;
|
|
110
|
+
|
|
111
|
+
const frameStack = ctx.__repro_frame_unawaited || (ctx.__repro_frame_unawaited = []);
|
|
112
|
+
const pendingQueue = ctx.__repro_pending_unawaited;
|
|
113
|
+
let frameUnawaited = false;
|
|
114
|
+
if (Array.isArray(pendingQueue) && pendingQueue.length) {
|
|
115
|
+
const marker = pendingQueue.shift();
|
|
116
|
+
frameUnawaited = !!(marker && marker.unawaited);
|
|
117
|
+
}
|
|
118
|
+
if (DEBUG_UNAWAITED) {
|
|
119
|
+
try { process.stderr.write(`[unawaited] enter ${fn} -> ${frameUnawaited}\n`); } catch {}
|
|
120
|
+
}
|
|
121
|
+
frameStack.push(frameUnawaited);
|
|
122
|
+
|
|
123
|
+
const fallbackDefinitionFile = meta?.file || null;
|
|
124
|
+
let file = meta?.file || null;
|
|
125
|
+
let line = meta?.line || null;
|
|
126
|
+
let sourceFile = meta?.sourceFile || null;
|
|
127
|
+
try {
|
|
128
|
+
const queue = ctx.__repro_callsite_queue;
|
|
129
|
+
if (Array.isArray(queue) && queue.length) {
|
|
130
|
+
const override = queue.shift();
|
|
131
|
+
if (override && (override.file || override.line !== null && override.line !== undefined)) {
|
|
132
|
+
if (!sourceFile && fallbackDefinitionFile) sourceFile = fallbackDefinitionFile;
|
|
133
|
+
if (override.file) file = override.file;
|
|
134
|
+
if (override.line !== null && override.line !== undefined) line = override.line;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch {}
|
|
138
|
+
|
|
139
|
+
const span = pushSpan(ctx, ctx.depth, parentSpanIdOverride);
|
|
140
|
+
try {
|
|
141
|
+
span.file = file;
|
|
142
|
+
span.line = line;
|
|
143
|
+
span.sourceFile = sourceFile;
|
|
144
|
+
} catch {}
|
|
145
|
+
|
|
146
|
+
emit({
|
|
147
|
+
type: 'enter',
|
|
148
|
+
t: Date.now(),
|
|
149
|
+
fn,
|
|
150
|
+
file,
|
|
151
|
+
line,
|
|
152
|
+
sourceFile,
|
|
153
|
+
functionType: meta?.functionType || null,
|
|
154
|
+
traceId: ctx.traceId,
|
|
155
|
+
depth: ctx.depth,
|
|
156
|
+
args: detail?.args,
|
|
157
|
+
spanId: span.id,
|
|
158
|
+
parentSpanId: span.parentId
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
exit(meta, detail){
|
|
162
|
+
const ctx = als.getStore() || {};
|
|
163
|
+
const depthAtExit = ctx.depth || 0;
|
|
164
|
+
const traceIdAtExit = ctx.traceId;
|
|
165
|
+
const baseMeta = {
|
|
166
|
+
fn: meta?.fn,
|
|
167
|
+
file: meta?.file,
|
|
168
|
+
line: meta?.line,
|
|
169
|
+
sourceFile: meta?.sourceFile || null,
|
|
170
|
+
functionType: meta?.functionType || null
|
|
171
|
+
};
|
|
172
|
+
const frameStack = ctx.__repro_frame_unawaited;
|
|
173
|
+
const frameUnawaited = Array.isArray(frameStack) && frameStack.length
|
|
174
|
+
? !!frameStack.pop()
|
|
175
|
+
: false;
|
|
176
|
+
const spanStackRef = Array.isArray(ctx.__repro_span_stack) ? ctx.__repro_span_stack : [];
|
|
177
|
+
const spanInfoPeek = spanStackRef.length
|
|
178
|
+
? spanStackRef[spanStackRef.length - 1]
|
|
179
|
+
: { id: null, parentId: null, depth: depthAtExit, file: baseMeta.file, line: baseMeta.line, sourceFile: baseMeta.sourceFile };
|
|
180
|
+
const baseDetail = {
|
|
181
|
+
args: detail?.args,
|
|
182
|
+
returnValue: detail?.returnValue,
|
|
183
|
+
error: detail?.error,
|
|
184
|
+
threw: detail?.threw === true,
|
|
185
|
+
unawaited: detail?.unawaited === true || frameUnawaited
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const promiseTaggedUnawaited = !!(baseDetail.returnValue && baseDetail.returnValue[SYM_UNAWAITED]);
|
|
189
|
+
const forceUnawaited = baseDetail.unawaited || promiseTaggedUnawaited;
|
|
190
|
+
|
|
191
|
+
const emitExit = (spanInfo, overrides = {}) => {
|
|
192
|
+
const finalDetail = {
|
|
193
|
+
returnValue: overrides.hasOwnProperty('returnValue')
|
|
194
|
+
? overrides.returnValue
|
|
195
|
+
: baseDetail.returnValue,
|
|
196
|
+
threw: overrides.hasOwnProperty('threw')
|
|
197
|
+
? overrides.threw
|
|
198
|
+
: baseDetail.threw,
|
|
199
|
+
error: overrides.hasOwnProperty('error')
|
|
200
|
+
? overrides.error
|
|
201
|
+
: baseDetail.error,
|
|
202
|
+
unawaited: overrides.hasOwnProperty('unawaited')
|
|
203
|
+
? overrides.unawaited
|
|
204
|
+
: forceUnawaited,
|
|
205
|
+
args: overrides.hasOwnProperty('args')
|
|
206
|
+
? overrides.args
|
|
207
|
+
: baseDetail.args
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
emit({
|
|
211
|
+
type: 'exit',
|
|
212
|
+
t: Date.now(),
|
|
213
|
+
fn: baseMeta.fn,
|
|
214
|
+
file: spanInfo.file !== null && spanInfo.file !== undefined ? spanInfo.file : baseMeta.file,
|
|
215
|
+
line: spanInfo.line !== null && spanInfo.line !== undefined ? spanInfo.line : baseMeta.line,
|
|
216
|
+
sourceFile: spanInfo.sourceFile !== null && spanInfo.sourceFile !== undefined ? spanInfo.sourceFile : baseMeta.sourceFile,
|
|
217
|
+
functionType: baseMeta.functionType || null,
|
|
218
|
+
traceId: traceIdAtExit,
|
|
219
|
+
depth: spanInfo.depth ?? depthAtExit,
|
|
220
|
+
spanId: spanInfo.id,
|
|
221
|
+
parentSpanId: spanInfo.parentId,
|
|
222
|
+
returnValue: finalDetail.returnValue,
|
|
223
|
+
threw: finalDetail.threw === true,
|
|
224
|
+
error: finalDetail.error,
|
|
225
|
+
args: finalDetail.args,
|
|
226
|
+
unawaited: finalDetail.unawaited === true
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const emitNow = (overrides = {}, spanForExitOverride = null, spanStackForExitOverride = null) => {
|
|
231
|
+
const spanStackCopy = spanStackForExitOverride
|
|
232
|
+
? spanStackForExitOverride.slice()
|
|
233
|
+
: Array.isArray(spanStackRef) ? spanStackRef.slice() : [];
|
|
234
|
+
const spanForExit = spanForExitOverride
|
|
235
|
+
? spanForExitOverride
|
|
236
|
+
: (popSpan(ctx) || spanInfoPeek);
|
|
237
|
+
ctx.depth = Math.max(0, (spanForExit.depth ?? depthAtExit) - 1);
|
|
238
|
+
|
|
239
|
+
const fn = () => emitExit(spanForExit, overrides);
|
|
240
|
+
if (!traceIdAtExit) return fn();
|
|
241
|
+
const store = { traceId: traceIdAtExit, depth: spanForExit.depth ?? depthAtExit, __repro_span_stack: spanStackCopy };
|
|
242
|
+
return als.run(store, fn);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
if (!baseDetail.threw) {
|
|
246
|
+
const rv = baseDetail.returnValue;
|
|
247
|
+
const isQuery = isMongooseQuery(rv);
|
|
248
|
+
|
|
249
|
+
if (isThenable(rv)) {
|
|
250
|
+
// Detach span immediately so downstream sync work doesn't inherit it.
|
|
251
|
+
const spanStackForExit = Array.isArray(ctx.__repro_span_stack)
|
|
252
|
+
? ctx.__repro_span_stack.slice()
|
|
253
|
+
: Array.isArray(spanStackRef) ? spanStackRef.slice() : [];
|
|
254
|
+
const spanForExit = popSpan(ctx) || spanInfoPeek;
|
|
255
|
+
ctx.depth = Math.max(0, (spanForExit.depth ?? depthAtExit) - 1);
|
|
256
|
+
|
|
257
|
+
let settled = false;
|
|
258
|
+
const finalize = (value, threw, error) => {
|
|
259
|
+
if (settled) return value;
|
|
260
|
+
settled = true;
|
|
261
|
+
const fn = () => emitExit(spanForExit, { returnValue: value, threw, error, unawaited: forceUnawaited });
|
|
262
|
+
if (!traceIdAtExit) return fn();
|
|
263
|
+
const store = { traceId: traceIdAtExit, depth: spanForExit.depth ?? depthAtExit, __repro_span_stack: spanStackForExit.slice() };
|
|
264
|
+
return als.run(store, fn);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const attachToPromise = (promise, allowQueryPromise = true) => {
|
|
268
|
+
if (!promise || typeof promise.then !== 'function') return false;
|
|
269
|
+
if (!allowQueryPromise && isMongooseQuery(promise)) return false;
|
|
270
|
+
try {
|
|
271
|
+
promise.then(
|
|
272
|
+
value => finalize(value, false, null),
|
|
273
|
+
err => finalize(undefined, true, err)
|
|
274
|
+
);
|
|
275
|
+
} catch (err) {
|
|
276
|
+
finalize(undefined, true, err);
|
|
277
|
+
}
|
|
278
|
+
return true;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
if (isQuery) {
|
|
282
|
+
const qp = rv && rv.__repro_result_promise;
|
|
283
|
+
if (attachToPromise(qp, false)) return;
|
|
284
|
+
if (Object.prototype.hasOwnProperty.call(rv || {}, '__repro_result')) {
|
|
285
|
+
finalize(rv.__repro_result, false, null);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (queueQueryFinalizer(rv, finalize)) return;
|
|
289
|
+
emitNow({ unawaited: forceUnawaited, returnValue: rv }, spanForExit, spanStackForExit);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (attachToPromise(rv, true)) return;
|
|
294
|
+
emitNow({ unawaited: forceUnawaited, returnValue: rv }, spanForExit, spanStackForExit);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (isQuery) {
|
|
299
|
+
emitNow({ unawaited: forceUnawaited });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
emitNow({ unawaited: forceUnawaited });
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
global.__trace = trace; // called by injected code
|
|
307
|
+
|
|
308
|
+
// ===== Symbols used by the loader to tag function origins =====
|
|
309
|
+
const SYM_SRC_FILE = Symbol.for('__repro_src_file'); // function's defining file (set by require hook)
|
|
310
|
+
const SYM_IS_APP = Symbol.for('__repro_is_app'); // boolean: true if function is from app code
|
|
311
|
+
const SYM_SKIP_WRAP= Symbol.for('__repro_skip_wrap'); // guard to avoid wrapping our own helpers
|
|
312
|
+
const SYM_BODY_TRACED = Symbol.for('__repro_body_traced'); // set on functions whose bodies already emit trace enter/exit
|
|
313
|
+
const SYM_UNAWAITED = Symbol.for('__repro_unawaited');
|
|
314
|
+
const SYM_PROMISE_STORE = Symbol.for('__repro_promise_store');
|
|
315
|
+
|
|
316
|
+
function emit(ev){
|
|
317
|
+
if (EMITTING) return;
|
|
318
|
+
EMITTING = true;
|
|
319
|
+
try { for (const l of listeners) l(ev); }
|
|
320
|
+
finally { EMITTING = false; }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
let loggerState = null;
|
|
324
|
+
let loggerBeforeExitInstalled = false;
|
|
325
|
+
|
|
326
|
+
function ensureFunctionLogger() {
|
|
327
|
+
if (loggerState) return loggerState;
|
|
328
|
+
|
|
329
|
+
// ---- filtered logger: full detail for app code, top-level only for node_modules ----
|
|
330
|
+
const isNodeModules = (file) => !!file && file.replace(/\\/g, '/').includes('/node_modules/');
|
|
331
|
+
|
|
332
|
+
// per-trace logger state
|
|
333
|
+
const stateByTrace = new Map();
|
|
334
|
+
function getState(traceId) {
|
|
335
|
+
const k = traceId || '__global__';
|
|
336
|
+
let s = stateByTrace.get(k);
|
|
337
|
+
if (!s) {
|
|
338
|
+
s = { stack: [], muteDepth: null, lastLine: null, repeat: 0 };
|
|
339
|
+
stateByTrace.set(k, s);
|
|
340
|
+
}
|
|
341
|
+
return s;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function flushRepeat(s) {
|
|
345
|
+
if (s.repeat > 1) process.stdout.write(` … ×${s.repeat - 1}\n`);
|
|
346
|
+
s.repeat = 0;
|
|
347
|
+
s.lastLine = null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function printLine(ev, st) {
|
|
351
|
+
const d = ev.depth || 0;
|
|
352
|
+
const indent = ' '.repeat(Math.max(0, d - (ev.type === 'exit' ? 1 : 0)));
|
|
353
|
+
const loc = ev.file ? ` (${short(ev.file)}:${ev.line ?? ''})` : '';
|
|
354
|
+
const id = ev.traceId ? ` [${ev.traceId}]` : '';
|
|
355
|
+
const line = ev.type === 'enter'
|
|
356
|
+
? `${indent}→ enter ${ev.fn}${loc}${id}`
|
|
357
|
+
: `${indent}← exit${id}`;
|
|
358
|
+
|
|
359
|
+
// coalesce exact repeats
|
|
360
|
+
if (line === st.lastLine) { st.repeat++; return; }
|
|
361
|
+
if (st.repeat > 0) flushRepeat(st);
|
|
362
|
+
process.stdout.write(line + '\n');
|
|
363
|
+
st.lastLine = line; st.repeat = 1;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Re-entrancy guard for emitting
|
|
367
|
+
let IN_LOG = false;
|
|
368
|
+
trace.on(ev => {
|
|
369
|
+
if (!functionLogsEnabled) return;
|
|
370
|
+
if (IN_LOG) return;
|
|
371
|
+
IN_LOG = true;
|
|
372
|
+
try {
|
|
373
|
+
const st = getState(ev.traceId);
|
|
374
|
+
const nm = isNodeModules(ev.file);
|
|
375
|
+
|
|
376
|
+
if (ev.type === 'enter') {
|
|
377
|
+
const prev = st.stack.length ? st.stack[st.stack.length - 1] : null;
|
|
378
|
+
const prevIsNM = prev ? prev.isNM : false;
|
|
379
|
+
|
|
380
|
+
// If we are already muting deeper node_modules frames, and this is another dep frame at/under mute depth -> skip
|
|
381
|
+
if (nm && st.muteDepth !== null && ev.depth >= st.muteDepth) {
|
|
382
|
+
st.stack.push({ isNM: true }); // keep structural parity
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Crossing app -> dep: print this top-level dep fn, then mute deeper dep frames
|
|
387
|
+
if (nm && !prevIsNM) {
|
|
388
|
+
printLine(ev, st);
|
|
389
|
+
st.muteDepth = ev.depth + 1;
|
|
390
|
+
st.stack.push({ isNM: true });
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// App code (or dep -> app bounce): always print
|
|
395
|
+
printLine(ev, st);
|
|
396
|
+
st.stack.push({ isNM: nm });
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// EXIT
|
|
401
|
+
if (ev.type === 'exit') {
|
|
402
|
+
const cur = st.stack.length ? st.stack[st.stack.length - 1] : null;
|
|
403
|
+
const curIsNM = cur ? cur.isNM : false;
|
|
404
|
+
|
|
405
|
+
// If this is a muted nested dep frame, skip printing
|
|
406
|
+
if (curIsNM && st.muteDepth !== null && ev.depth >= st.muteDepth) {
|
|
407
|
+
st.stack.pop();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Print exits for app frames and for the top-level dep frame
|
|
412
|
+
printLine(ev, st);
|
|
413
|
+
|
|
414
|
+
// If we just exited the top-level dep frame, unmute deeper deps
|
|
415
|
+
if (curIsNM && st.muteDepth !== null && ev.depth === st.muteDepth - 1) {
|
|
416
|
+
st.muteDepth = null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
st.stack.pop();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
} finally {
|
|
423
|
+
IN_LOG = false;
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
if (!loggerBeforeExitInstalled) {
|
|
428
|
+
// flush any coalesced repeats before exiting
|
|
429
|
+
process.on('beforeExit', () => {
|
|
430
|
+
for (const s of stateByTrace.values()) flushRepeat(s);
|
|
431
|
+
});
|
|
432
|
+
loggerBeforeExitInstalled = true;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
loggerState = { stateByTrace };
|
|
436
|
+
return loggerState;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function setFunctionLogsEnabled(enabled) {
|
|
440
|
+
functionLogsEnabled = !!enabled;
|
|
441
|
+
if (functionLogsEnabled) ensureFunctionLogger();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (functionLogsEnabled) ensureFunctionLogger();
|
|
445
|
+
|
|
446
|
+
function short(p){ try{ const cwd = process.cwd().replace(/\\/g,'/'); return String(p).replace(cwd+'/',''); } catch { return p; } }
|
|
447
|
+
|
|
448
|
+
function markPromiseUnawaited(value) {
|
|
449
|
+
if (!value || (typeof value !== 'object' && typeof value !== 'function')) return;
|
|
450
|
+
try {
|
|
451
|
+
Object.defineProperty(value, SYM_UNAWAITED, { value: true, configurable: true });
|
|
452
|
+
} catch {
|
|
453
|
+
try { value[SYM_UNAWAITED] = true; } catch {}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function cloneStore(baseStore) {
|
|
458
|
+
if (!baseStore) {
|
|
459
|
+
return {
|
|
460
|
+
traceId: null,
|
|
461
|
+
depth: 0,
|
|
462
|
+
__repro_span_stack: [],
|
|
463
|
+
__repro_frame_unawaited: [],
|
|
464
|
+
__repro_pending_unawaited: []
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
const stack = Array.isArray(baseStore.__repro_span_stack) ? baseStore.__repro_span_stack.slice() : [];
|
|
468
|
+
return {
|
|
469
|
+
traceId: baseStore.traceId || null,
|
|
470
|
+
depth: typeof baseStore.depth === 'number'
|
|
471
|
+
? baseStore.depth
|
|
472
|
+
: (stack.length ? stack[stack.length - 1].depth || stack.length : 0),
|
|
473
|
+
__repro_span_stack: stack,
|
|
474
|
+
__repro_frame_unawaited: [],
|
|
475
|
+
__repro_pending_unawaited: []
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const CWD = process.cwd().replace(/\\/g, '/');
|
|
480
|
+
const isNodeModulesPath = (p) => !!p && p.replace(/\\/g, '/').includes('/node_modules/');
|
|
481
|
+
const isAppPath = (p) => !!p && p.replace(/\\/g, '/').startsWith(CWD + '/') && !isNodeModulesPath(p);
|
|
482
|
+
|
|
483
|
+
function isProbablyAsyncFunction(fn) {
|
|
484
|
+
if (!fn || typeof fn !== 'function') return false;
|
|
485
|
+
const ctorName = fn.constructor && fn.constructor.name;
|
|
486
|
+
if (ctorName === 'AsyncFunction' || ctorName === 'AsyncGeneratorFunction') return true;
|
|
487
|
+
const tag = fn[Symbol.toStringTag];
|
|
488
|
+
if (tag === 'AsyncFunction' || tag === 'AsyncGeneratorFunction') return true;
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function forkAlsStoreForUnawaited(baseStore) {
|
|
493
|
+
if (!baseStore) return null;
|
|
494
|
+
const stack = Array.isArray(baseStore.__repro_span_stack)
|
|
495
|
+
? baseStore.__repro_span_stack.filter(s => s && s.__repro_suspended !== true)
|
|
496
|
+
: [];
|
|
497
|
+
const cloned = cloneStore(baseStore);
|
|
498
|
+
cloned.__repro_span_stack = stack.slice();
|
|
499
|
+
return cloned;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ========= Generic call-site shim (used by Babel transform) =========
|
|
503
|
+
// Decides whether to emit a top-level event based on callee origin tags.
|
|
504
|
+
// No hardcoded library names or file paths.
|
|
505
|
+
if (!global.__repro_call) {
|
|
506
|
+
Object.defineProperty(global, '__repro_call', {
|
|
507
|
+
value: function __repro_call(fn, thisArg, args, callFile, callLine, label, isUnawaitedCall) {
|
|
508
|
+
try {
|
|
509
|
+
if (typeof fn !== 'function' || fn[SYM_SKIP_WRAP]) {
|
|
510
|
+
return fn.apply(thisArg, args);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const currentStore = als.getStore();
|
|
514
|
+
const dbgName = (label && label.length) || (fn && fn.name) || '(anonymous)';
|
|
515
|
+
|
|
516
|
+
// If no tracing context is active, bail out quickly to avoid touching app semantics
|
|
517
|
+
// during module initialization or other untracked code paths.
|
|
518
|
+
if (!currentStore) {
|
|
519
|
+
try {
|
|
520
|
+
const out = fn.apply(thisArg, args);
|
|
521
|
+
if (isUnawaitedCall && isThenable(out)) markPromiseUnawaited(out);
|
|
522
|
+
return out;
|
|
523
|
+
} catch {
|
|
524
|
+
return fn ? fn.apply(thisArg, args) : undefined;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const sourceFile = fn[SYM_SRC_FILE];
|
|
529
|
+
let isApp = fn[SYM_IS_APP] === true;
|
|
530
|
+
if (!isApp) {
|
|
531
|
+
const appBySource = isAppPath(sourceFile);
|
|
532
|
+
if (appBySource && !isNativeFunction(fn)) {
|
|
533
|
+
isApp = true;
|
|
534
|
+
try { Object.defineProperty(fn, SYM_IS_APP, { value: true, configurable: true }); } catch {}
|
|
535
|
+
} else if (!sourceFile && callFile && isAppPath(callFile) && !isNativeFunction(fn)) {
|
|
536
|
+
isApp = true;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const instrumented = isFunctionInstrumented(fn);
|
|
540
|
+
const bodyTraced = !!fn[SYM_BODY_TRACED] || fn.__repro_body_traced === true;
|
|
541
|
+
|
|
542
|
+
// Capture parent context; derive parent span/depth from current ALS store.
|
|
543
|
+
const parentStore = currentStore || null;
|
|
544
|
+
const parentStack = parentStore && Array.isArray(parentStore.__repro_span_stack)
|
|
545
|
+
? parentStore.__repro_span_stack.slice()
|
|
546
|
+
: [];
|
|
547
|
+
const parentSpan = parentStack.length ? parentStack[parentStack.length - 1] : null;
|
|
548
|
+
const baseDepth = parentStore && typeof parentStore.depth === 'number'
|
|
549
|
+
? parentStore.depth
|
|
550
|
+
: (parentStack.length ? parentStack[parentStack.length - 1].depth || parentStack.length : 0);
|
|
551
|
+
|
|
552
|
+
// Build a minimal store for this call and run the call inside it.
|
|
553
|
+
const makeCallStore = () => ({
|
|
554
|
+
traceId: parentStore ? parentStore.traceId : null,
|
|
555
|
+
depth: baseDepth,
|
|
556
|
+
__repro_span_stack: parentStack.slice(),
|
|
557
|
+
__repro_frame_unawaited: [],
|
|
558
|
+
__repro_pending_unawaited: []
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const wrapArgWithStore = (arg, baseStoreSnapshot) => {
|
|
562
|
+
if (typeof arg !== 'function') return arg;
|
|
563
|
+
if (arg[SYM_SKIP_WRAP] || arg.__repro_arg_wrapper) return arg;
|
|
564
|
+
const hasContext = baseStoreSnapshot
|
|
565
|
+
&& (baseStoreSnapshot.traceId != null
|
|
566
|
+
|| (Array.isArray(baseStoreSnapshot.__repro_span_stack) && baseStoreSnapshot.__repro_span_stack.length));
|
|
567
|
+
if (!hasContext) return arg;
|
|
568
|
+
const wrapped = function reproWrappedArg() {
|
|
569
|
+
const perCallStore = cloneStore(baseStoreSnapshot);
|
|
570
|
+
let result;
|
|
571
|
+
als.run(perCallStore, () => {
|
|
572
|
+
// Support both callbacks and constructors: some libraries (e.g., class-transformer)
|
|
573
|
+
// pass class constructors as args and invoke them with `new`.
|
|
574
|
+
if (new.target) {
|
|
575
|
+
result = Reflect.construct(arg, arguments, arg);
|
|
576
|
+
} else {
|
|
577
|
+
result = arg.apply(this, arguments);
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
return result;
|
|
581
|
+
};
|
|
582
|
+
try { Object.defineProperty(wrapped, '__repro_arg_wrapper', { value: true }); } catch {}
|
|
583
|
+
try { wrapped[SYM_SKIP_WRAP] = true; } catch {}
|
|
584
|
+
return wrapped;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const wrapArgsWithStore = (argsArr, baseStoreSnapshot) => {
|
|
588
|
+
if (!Array.isArray(argsArr)) return argsArr;
|
|
589
|
+
return argsArr.map(a => wrapArgWithStore(a, baseStoreSnapshot));
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const isAsyncLocalContextSetter = (targetFn, targetThis) => {
|
|
593
|
+
if (!targetFn) return false;
|
|
594
|
+
if (targetFn === trace.withTrace) return true;
|
|
595
|
+
if (targetFn === als.run || targetFn === AsyncLocalStorage.prototype.run) return true;
|
|
596
|
+
if (targetFn === als.enterWith || targetFn === AsyncLocalStorage.prototype.enterWith) return true;
|
|
597
|
+
const name = targetFn.name;
|
|
598
|
+
const isALS = targetThis && targetThis instanceof AsyncLocalStorage;
|
|
599
|
+
if (isALS && (name === 'run' || name === 'enterWith')) return true;
|
|
600
|
+
return false;
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
const runWithCallStore = (fnToRun) => {
|
|
604
|
+
const callStore = makeCallStore();
|
|
605
|
+
const parentSpanId = callStore.__repro_span_stack.length
|
|
606
|
+
? callStore.__repro_span_stack[callStore.__repro_span_stack.length - 1].id
|
|
607
|
+
: null;
|
|
608
|
+
let out;
|
|
609
|
+
als.run(callStore, () => { out = fnToRun(parentSpanId, callStore); });
|
|
610
|
+
if (parentStore) {
|
|
611
|
+
try { als.enterWith(parentStore); } catch {}
|
|
612
|
+
}
|
|
613
|
+
return out;
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const invokeWithTrace = (forcedParentSpanId, callStoreForArgs) => {
|
|
617
|
+
const ctx = als.getStore();
|
|
618
|
+
let pendingMarker = null;
|
|
619
|
+
if (ctx && isUnawaitedCall) {
|
|
620
|
+
const queue = ctx.__repro_pending_unawaited || (ctx.__repro_pending_unawaited = []);
|
|
621
|
+
pendingMarker = { unawaited: true, id: Symbol('unawaited') };
|
|
622
|
+
queue.push(pendingMarker);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const name = (label && label.length) || fn.name
|
|
626
|
+
? (label && label.length ? label : fn.name)
|
|
627
|
+
: '(anonymous)';
|
|
628
|
+
const definitionFile = fn[SYM_SRC_FILE];
|
|
629
|
+
const normalizedCallFile = callFile ? String(callFile).trim() : '';
|
|
630
|
+
const callsiteFile = normalizedCallFile ? normalizedCallFile : null;
|
|
631
|
+
const callsiteLine = Number.isFinite(Number(callLine)) && Number(callLine) > 0 ? Number(callLine) : null;
|
|
632
|
+
const meta = {
|
|
633
|
+
file: callsiteFile || definitionFile || null,
|
|
634
|
+
line: callsiteLine,
|
|
635
|
+
sourceFile: definitionFile || null,
|
|
636
|
+
parentSpanId: forcedParentSpanId
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
trace.enter(name, meta, { args });
|
|
640
|
+
// After entering, snapshot a clean store that captures the parent + this call’s span,
|
|
641
|
+
// so callbacks invoked during the callee run are isolated per invocation.
|
|
642
|
+
const snapshotForArgs = cloneStore(als.getStore());
|
|
643
|
+
const shouldWrap = !isAsyncLocalContextSetter(fn, thisArg);
|
|
644
|
+
const argsForCall = shouldWrap ? wrapArgsWithStore(args, snapshotForArgs) : args;
|
|
645
|
+
try {
|
|
646
|
+
const out = fn.apply(thisArg, argsForCall);
|
|
647
|
+
|
|
648
|
+
const isThenableValue = isThenable(out);
|
|
649
|
+
const isQuery = isMongooseQuery(out);
|
|
650
|
+
const shouldForceExit = !!isUnawaitedCall && isThenableValue;
|
|
651
|
+
const exitDetailBase = {
|
|
652
|
+
returnValue: out,
|
|
653
|
+
args,
|
|
654
|
+
unawaited: shouldForceExit
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
if (shouldForceExit) markPromiseUnawaited(out);
|
|
658
|
+
|
|
659
|
+
const exitStore = (() => {
|
|
660
|
+
try {
|
|
661
|
+
const store = als.getStore();
|
|
662
|
+
return store ? forkAlsStoreForUnawaited(store) : null;
|
|
663
|
+
} catch {
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
})();
|
|
667
|
+
|
|
668
|
+
const runExit = (detail) => {
|
|
669
|
+
const runner = () => trace.exit({ fn: name, file: meta.file, line: meta.line, sourceFile: meta.sourceFile }, detail);
|
|
670
|
+
if (exitStore) return als.run(exitStore, runner);
|
|
671
|
+
return runner();
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
if (isThenableValue) {
|
|
675
|
+
// Attach store so downstream promise callbacks can restore context even if ALS loses it.
|
|
676
|
+
try {
|
|
677
|
+
if (out && typeof out === 'object') {
|
|
678
|
+
Object.defineProperty(out, SYM_PROMISE_STORE, { value: cloneStore(als.getStore()), configurable: true });
|
|
679
|
+
}
|
|
680
|
+
} catch {}
|
|
681
|
+
|
|
682
|
+
if (isQuery) {
|
|
683
|
+
runExit(exitDetailBase);
|
|
684
|
+
return out;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
let settled = false;
|
|
688
|
+
const finalize = (value, threw, error) => {
|
|
689
|
+
if (settled) return value;
|
|
690
|
+
settled = true;
|
|
691
|
+
const detail = {
|
|
692
|
+
returnValue: value,
|
|
693
|
+
args,
|
|
694
|
+
threw,
|
|
695
|
+
error,
|
|
696
|
+
unawaited: shouldForceExit
|
|
697
|
+
};
|
|
698
|
+
runExit(detail);
|
|
699
|
+
return value;
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
try {
|
|
703
|
+
out.then(
|
|
704
|
+
value => finalize(value, false, null),
|
|
705
|
+
err => finalize(undefined, true, err)
|
|
706
|
+
);
|
|
707
|
+
} catch (err) {
|
|
708
|
+
finalize(undefined, true, err);
|
|
709
|
+
}
|
|
710
|
+
return out;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
runExit(exitDetailBase);
|
|
714
|
+
return out;
|
|
715
|
+
} catch (e) {
|
|
716
|
+
trace.exit({ fn: name, file: meta.file, line: meta.line, sourceFile: meta.sourceFile }, { threw: true, error: e, args });
|
|
717
|
+
throw e;
|
|
718
|
+
} finally {
|
|
719
|
+
if (pendingMarker) {
|
|
720
|
+
const store = als.getStore();
|
|
721
|
+
const queue = store && store.__repro_pending_unawaited;
|
|
722
|
+
if (Array.isArray(queue)) {
|
|
723
|
+
const idx = queue.indexOf(pendingMarker);
|
|
724
|
+
if (idx !== -1) queue.splice(idx, 1);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
return runWithCallStore((forcedParentSpanId, callStoreForArgs) => {
|
|
731
|
+
if (bodyTraced) {
|
|
732
|
+
// Already emits enter/exit inside the function body — keep tracing context stable but skip call-level enter/exit.
|
|
733
|
+
const ctx = als.getStore();
|
|
734
|
+
let pendingMarker = null;
|
|
735
|
+
if (ctx && isUnawaitedCall) {
|
|
736
|
+
const queue = ctx.__repro_pending_unawaited || (ctx.__repro_pending_unawaited = []);
|
|
737
|
+
pendingMarker = { unawaited: true, id: Symbol('unawaited') };
|
|
738
|
+
queue.push(pendingMarker);
|
|
739
|
+
}
|
|
740
|
+
// For unawaited calls, isolate into a forked store so child spans don't leak into the caller's stack.
|
|
741
|
+
const isolatedStore = isUnawaitedCall
|
|
742
|
+
? (forkAlsStoreForUnawaited(callStoreForArgs || ctx) || cloneStore(callStoreForArgs || ctx))
|
|
743
|
+
: null;
|
|
744
|
+
try {
|
|
745
|
+
const shouldWrap = !isAsyncLocalContextSetter(fn, thisArg);
|
|
746
|
+
const argsForCall = shouldWrap ? wrapArgsWithStore(args, callStoreForArgs || ctx) : args;
|
|
747
|
+
const normalizedCallFile = callFile ? String(callFile).trim() : '';
|
|
748
|
+
const callsiteFile = normalizedCallFile ? normalizedCallFile : null;
|
|
749
|
+
const callsiteLine = Number.isFinite(Number(callLine)) && Number(callLine) > 0 ? Number(callLine) : null;
|
|
750
|
+
const callsiteStore = isolatedStore || ctx;
|
|
751
|
+
if (callsiteStore && (callsiteFile || callsiteLine !== null)) {
|
|
752
|
+
const queue = callsiteStore.__repro_callsite_queue || (callsiteStore.__repro_callsite_queue = []);
|
|
753
|
+
queue.push({ file: callsiteFile, line: callsiteLine });
|
|
754
|
+
}
|
|
755
|
+
let out;
|
|
756
|
+
if (isolatedStore) {
|
|
757
|
+
als.run(isolatedStore, () => {
|
|
758
|
+
out = fn.apply(thisArg, argsForCall);
|
|
759
|
+
});
|
|
760
|
+
} else {
|
|
761
|
+
out = fn.apply(thisArg, argsForCall);
|
|
762
|
+
}
|
|
763
|
+
if (isUnawaitedCall && isThenable(out)) markPromiseUnawaited(out);
|
|
764
|
+
return out;
|
|
765
|
+
} finally {
|
|
766
|
+
if (pendingMarker) {
|
|
767
|
+
const store = als.getStore();
|
|
768
|
+
const queue = store && store.__repro_pending_unawaited;
|
|
769
|
+
if (Array.isArray(queue)) {
|
|
770
|
+
const idx = queue.indexOf(pendingMarker);
|
|
771
|
+
if (idx !== -1) queue.splice(idx, 1);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return invokeWithTrace(forcedParentSpanId, callStoreForArgs);
|
|
777
|
+
});
|
|
778
|
+
} catch {
|
|
779
|
+
return fn ? fn.apply(thisArg, args) : undefined;
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
configurable: false,
|
|
783
|
+
writable: false,
|
|
784
|
+
enumerable: false
|
|
785
|
+
});
|
|
786
|
+
// Guard our helper from any instrumentation
|
|
787
|
+
global.__repro_call[SYM_SKIP_WRAP] = true;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// ---- automatic per-request context via http/https ----
|
|
791
|
+
function patchHttp(){
|
|
792
|
+
try {
|
|
793
|
+
const http = require('node:http');
|
|
794
|
+
const Server = http.Server;
|
|
795
|
+
const _emit = Server.prototype.emit;
|
|
796
|
+
Server.prototype.emit = function(ev, req, res){
|
|
797
|
+
if (ev === 'request' && req && res) {
|
|
798
|
+
const id = `${req.method} ${req.url} #${(Math.random()*1e9|0).toString(36)}`;
|
|
799
|
+
return trace.withTrace(id, () => _emit.call(this, ev, req, res));
|
|
800
|
+
}
|
|
801
|
+
return _emit.apply(this, arguments);
|
|
802
|
+
};
|
|
803
|
+
// https piggybacks http.Server in Node, no extra patch usually needed
|
|
804
|
+
} catch {}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// ---- isolate array iterator callbacks so parallel async branches don't share the same ALS store ----
|
|
808
|
+
let ARRAY_ITER_PATCHED = false;
|
|
809
|
+
function patchArrayIterators() {
|
|
810
|
+
if (ARRAY_ITER_PATCHED) return;
|
|
811
|
+
ARRAY_ITER_PATCHED = true;
|
|
812
|
+
|
|
813
|
+
const methods = ['map', 'forEach', 'filter', 'flatMap', 'reduce'];
|
|
814
|
+
for (const name of methods) {
|
|
815
|
+
const orig = Array.prototype[name];
|
|
816
|
+
if (typeof orig !== 'function') continue;
|
|
817
|
+
|
|
818
|
+
Object.defineProperty(Array.prototype, name, {
|
|
819
|
+
configurable: true,
|
|
820
|
+
writable: true,
|
|
821
|
+
value: function reproArrayIter(cb /*, ...rest */) {
|
|
822
|
+
if (typeof cb !== 'function') return orig.apply(this, arguments);
|
|
823
|
+
const store = als.getStore();
|
|
824
|
+
if (!store) return orig.apply(this, arguments);
|
|
825
|
+
|
|
826
|
+
const rest = Array.prototype.slice.call(arguments, 1);
|
|
827
|
+
const wrappedCb = function reproIterCb() {
|
|
828
|
+
const perIterStore = cloneStore(store);
|
|
829
|
+
let res;
|
|
830
|
+
als.run(perIterStore, () => {
|
|
831
|
+
res = cb.apply(this, arguments);
|
|
832
|
+
});
|
|
833
|
+
return res;
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
return orig.apply(this, [wrappedCb, ...rest]);
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// ---- optional V8 sampling summary on SIGINT ----
|
|
843
|
+
let inspectorSession = null;
|
|
844
|
+
function startV8(samplingMs = 10){
|
|
845
|
+
const inspector = require('node:inspector');
|
|
846
|
+
inspectorSession = new inspector.Session();
|
|
847
|
+
inspectorSession.connect();
|
|
848
|
+
inspectorSession.post('Profiler.enable');
|
|
849
|
+
inspectorSession.post('Profiler.setSamplingInterval', { interval: samplingMs * 1000 });
|
|
850
|
+
inspectorSession.post('Profiler.start');
|
|
851
|
+
if (!quietEnv) process.stdout.write(`[v8] profiler started @ ${samplingMs}ms\n`);
|
|
852
|
+
}
|
|
853
|
+
function stopV8(){ return new Promise((resolve, reject) => {
|
|
854
|
+
if (!inspectorSession) return resolve(null);
|
|
855
|
+
inspectorSession.post('Profiler.stop', (err, payload) => {
|
|
856
|
+
if (err) return reject(err);
|
|
857
|
+
try { inspectorSession.disconnect(); } catch {}
|
|
858
|
+
inspectorSession = null;
|
|
859
|
+
resolve(payload?.profile ?? null);
|
|
860
|
+
});
|
|
861
|
+
});}
|
|
862
|
+
function summarize(profile, topN=10){
|
|
863
|
+
if (!profile) return { top: [] };
|
|
864
|
+
const nodes = new Map(profile.nodes.map(n=>[n.id,n]));
|
|
865
|
+
const { samples=[], timeDeltas=[] } = profile;
|
|
866
|
+
const self = new Map();
|
|
867
|
+
for (let i=0;i<samples.length;i++) self.set(samples[i], (self.get(samples[i])||0)+(timeDeltas[i]||0));
|
|
868
|
+
const top = [...self].map(([id,us])=>({node:nodes.get(id),ms:us/1000}))
|
|
869
|
+
.sort((a,b)=>b.ms-a.ms).slice(0,topN)
|
|
870
|
+
.map(({node,ms})=>({ ms:+ms.toFixed(2),
|
|
871
|
+
fn: node?.callFrame?.functionName || '(anonymous)',
|
|
872
|
+
url: node?.callFrame?.url, line: node?.callFrame?.lineNumber!=null ? node.callFrame.lineNumber+1 : undefined }));
|
|
873
|
+
return { top };
|
|
874
|
+
}
|
|
875
|
+
async function printV8(){ const p=await stopV8(); const s=summarize(p);
|
|
876
|
+
if (!quietEnv) { process.stdout.write('\n[v8] Top self-time:\n');
|
|
877
|
+
for (const r of s.top) process.stdout.write(` ${r.ms}ms ${r.fn} ${r.url ?? ''}:${r.line ?? ''}\n`);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function getCurrentTraceId() {
|
|
882
|
+
const s = als.getStore();
|
|
883
|
+
return s && s.traceId || null;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function getCurrentSpanContext() {
|
|
887
|
+
try {
|
|
888
|
+
const store = als.getStore();
|
|
889
|
+
if (!store) return null;
|
|
890
|
+
|
|
891
|
+
const stack = Array.isArray(store.__repro_span_stack) ? store.__repro_span_stack : [];
|
|
892
|
+
const top = stack.length ? stack[stack.length - 1] : null;
|
|
893
|
+
const spanId = top && top.id != null ? top.id : null;
|
|
894
|
+
const parentSpanId = top && top.parentId != null
|
|
895
|
+
? top.parentId
|
|
896
|
+
: (stack.length >= 2 ? (stack[stack.length - 2]?.id ?? null) : null);
|
|
897
|
+
const depth = top && top.depth != null
|
|
898
|
+
? top.depth
|
|
899
|
+
: (typeof store.depth === 'number'
|
|
900
|
+
? store.depth
|
|
901
|
+
: (stack.length ? stack.length : null));
|
|
902
|
+
|
|
903
|
+
const traceId = store.traceId || null;
|
|
904
|
+
|
|
905
|
+
if (spanId === null && parentSpanId === null && traceId === null) return null;
|
|
906
|
+
return { traceId, spanId, parentSpanId, depth: depth == null ? null : depth };
|
|
907
|
+
} catch {
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// ---- Promise propagation fallback: ensure continuations run with the captured store ----
|
|
913
|
+
let PROMISE_PATCHED = false;
|
|
914
|
+
function patchPromise() {
|
|
915
|
+
if (PROMISE_PATCHED) return;
|
|
916
|
+
PROMISE_PATCHED = true;
|
|
917
|
+
|
|
918
|
+
const wrapCb = (cb) => {
|
|
919
|
+
if (typeof cb !== 'function') return cb;
|
|
920
|
+
const activeStore = als.getStore() || this?.[SYM_PROMISE_STORE] || null;
|
|
921
|
+
if (!activeStore) return cb;
|
|
922
|
+
const snapshot = cloneStore(activeStore);
|
|
923
|
+
const wrapped = function reproPromiseCb() {
|
|
924
|
+
let res;
|
|
925
|
+
als.run(snapshot, () => { res = cb.apply(this, arguments); });
|
|
926
|
+
return res;
|
|
927
|
+
};
|
|
928
|
+
try { wrapped[SYM_SKIP_WRAP] = true; } catch {}
|
|
929
|
+
return wrapped;
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
const origThen = Promise.prototype.then;
|
|
933
|
+
const origCatch = Promise.prototype.catch;
|
|
934
|
+
const origFinally = Promise.prototype.finally;
|
|
935
|
+
|
|
936
|
+
Promise.prototype.then = function reproThen(onFulfilled, onRejected) {
|
|
937
|
+
return origThen.call(this, wrapCb.call(this, onFulfilled), wrapCb.call(this, onRejected));
|
|
938
|
+
};
|
|
939
|
+
Promise.prototype.catch = function reproCatch(onRejected) {
|
|
940
|
+
return origCatch.call(this, wrapCb.call(this, onRejected));
|
|
941
|
+
};
|
|
942
|
+
Promise.prototype.finally = function reproFinally(onFinally) {
|
|
943
|
+
return origFinally.call(this, wrapCb.call(this, onFinally));
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
module.exports = {
|
|
948
|
+
trace,
|
|
949
|
+
patchHttp,
|
|
950
|
+
patchArrayIterators,
|
|
951
|
+
patchPromise,
|
|
952
|
+
startV8,
|
|
953
|
+
printV8,
|
|
954
|
+
patchConsole,
|
|
955
|
+
getCurrentTraceId,
|
|
956
|
+
getCurrentSpanContext,
|
|
957
|
+
setFunctionLogsEnabled,
|
|
958
|
+
// export symbols so the require hook can tag function origins
|
|
959
|
+
SYM_SRC_FILE,
|
|
960
|
+
SYM_IS_APP,
|
|
961
|
+
SYM_SKIP_WRAP,
|
|
962
|
+
SYM_BODY_TRACED
|
|
963
|
+
};
|