@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.
@@ -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
+ };