@reproapp/node-sdk 0.0.1
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/README.md +343 -0
- package/dist/index.d.ts +221 -0
- package/dist/index.js +2535 -0
- package/dist/integrations/sendgrid.d.ts +20 -0
- package/dist/integrations/sendgrid.js +177 -0
- package/dist/redaction.d.ts +44 -0
- package/dist/redaction.js +167 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +26 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2535 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.patchSendgridMail = exports.reproMongoosePlugin = exports.reproMiddleware = exports.isReproTracingEnabled = exports.initReproTracing = exports.disableReproTraceLogs = exports.enableReproTraceLogs = exports.setReproTraceLogsEnabled = exports.setDisabledTraceFiles = exports.setDisabledFunctionTypes = exports.setDisabledFunctionTraces = void 0;
|
|
4
|
+
const mongoose = require("mongoose");
|
|
5
|
+
const async_hooks_1 = require("async_hooks");
|
|
6
|
+
/**
|
|
7
|
+
* ============================= IMPORTANT =============================
|
|
8
|
+
* We snapshot Mongoose core methods BEFORE tracer init, then re-apply them
|
|
9
|
+
* AFTER tracer init. This guarantees Model.find() still returns a Query and
|
|
10
|
+
* chainability (e.g., .sort().lean()) is preserved. No behavior changes.
|
|
11
|
+
* =====================================================================
|
|
12
|
+
*/
|
|
13
|
+
const __ORIG = (() => {
|
|
14
|
+
const M = mongoose.Model;
|
|
15
|
+
const Qp = mongoose.Query?.prototype;
|
|
16
|
+
const Ap = mongoose.Aggregate?.prototype;
|
|
17
|
+
return {
|
|
18
|
+
Model: {
|
|
19
|
+
find: M?.find,
|
|
20
|
+
findOne: M?.findOne,
|
|
21
|
+
findById: M?.findById,
|
|
22
|
+
create: M?.create,
|
|
23
|
+
insertMany: M?.insertMany,
|
|
24
|
+
updateOne: M?.updateOne,
|
|
25
|
+
updateMany: M?.updateMany,
|
|
26
|
+
replaceOne: M?.replaceOne,
|
|
27
|
+
deleteOne: M?.deleteOne,
|
|
28
|
+
deleteMany: M?.deleteMany,
|
|
29
|
+
countDocuments: M?.countDocuments,
|
|
30
|
+
estimatedDocumentCount: M?.estimatedDocumentCount,
|
|
31
|
+
distinct: M?.distinct,
|
|
32
|
+
findOneAndUpdate: M?.findOneAndUpdate,
|
|
33
|
+
findOneAndDelete: M?.findOneAndDelete,
|
|
34
|
+
findOneAndReplace: M?.findOneAndReplace,
|
|
35
|
+
findOneAndRemove: M?.findOneAndRemove,
|
|
36
|
+
bulkWrite: M?.bulkWrite,
|
|
37
|
+
},
|
|
38
|
+
Query: {
|
|
39
|
+
exec: Qp?.exec,
|
|
40
|
+
lean: Qp?.lean,
|
|
41
|
+
sort: Qp?.sort,
|
|
42
|
+
select: Qp?.select,
|
|
43
|
+
limit: Qp?.limit,
|
|
44
|
+
skip: Qp?.skip,
|
|
45
|
+
populate: Qp?.populate,
|
|
46
|
+
getFilter: Qp?.getFilter,
|
|
47
|
+
getUpdate: Qp?.getUpdate,
|
|
48
|
+
getOptions: Qp?.getOptions,
|
|
49
|
+
projection: Qp?.projection,
|
|
50
|
+
},
|
|
51
|
+
Aggregate: {
|
|
52
|
+
exec: Ap?.exec,
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
})();
|
|
56
|
+
function restoreMongooseIfNeeded() {
|
|
57
|
+
try {
|
|
58
|
+
const M = mongoose.Model;
|
|
59
|
+
const Qp = mongoose.Query?.prototype;
|
|
60
|
+
const Ap = mongoose.Aggregate?.prototype;
|
|
61
|
+
const safeSet = (obj, key, val) => {
|
|
62
|
+
if (!obj || !val)
|
|
63
|
+
return;
|
|
64
|
+
if (obj[key] !== val) {
|
|
65
|
+
try {
|
|
66
|
+
obj[key] = val;
|
|
67
|
+
}
|
|
68
|
+
catch { }
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
// Restore Model methods (chainability depends on these returning Query)
|
|
72
|
+
Object.entries(__ORIG.Model).forEach(([k, v]) => safeSet(M, k, v));
|
|
73
|
+
// Restore Query prototype essentials (exec/lean/etc.)
|
|
74
|
+
Object.entries(__ORIG.Query).forEach(([k, v]) => safeSet(Qp, k, v));
|
|
75
|
+
// Restore Aggregate exec
|
|
76
|
+
Object.entries(__ORIG.Aggregate).forEach(([k, v]) => safeSet(Ap, k, v));
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
}
|
|
80
|
+
function flushQueryFinalizers(query, value, threw, error) {
|
|
81
|
+
try {
|
|
82
|
+
const callbacks = query?.__repro_query_finalizers;
|
|
83
|
+
if (!Array.isArray(callbacks) || callbacks.length === 0)
|
|
84
|
+
return;
|
|
85
|
+
query.__repro_query_finalizers = [];
|
|
86
|
+
for (const fn of callbacks) {
|
|
87
|
+
try {
|
|
88
|
+
fn(value, threw, error);
|
|
89
|
+
}
|
|
90
|
+
catch { }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch { }
|
|
94
|
+
}
|
|
95
|
+
function patchMongooseExecCapture(targetMongoose = mongoose) {
|
|
96
|
+
// Patch Query.prototype.exec to capture resolved results for trace return values.
|
|
97
|
+
try {
|
|
98
|
+
const Qp = targetMongoose?.Query?.prototype;
|
|
99
|
+
if (Qp && !Qp.__repro_exec_patched) {
|
|
100
|
+
const origExec = Qp.exec;
|
|
101
|
+
if (typeof origExec === 'function') {
|
|
102
|
+
Qp.__repro_exec_patched = true;
|
|
103
|
+
Qp.exec = function reproPatchedExec(...args) {
|
|
104
|
+
try {
|
|
105
|
+
this.__repro_is_query = true;
|
|
106
|
+
const ctx = __TRACER__?.getCurrentSpanContext?.();
|
|
107
|
+
const traceId = ctx?.traceId ?? __TRACER__?.getCurrentTraceId?.() ?? null;
|
|
108
|
+
if (ctx || traceId) {
|
|
109
|
+
Object.defineProperty(this, '__repro_span_context', {
|
|
110
|
+
value: {
|
|
111
|
+
traceId,
|
|
112
|
+
spanId: ctx?.spanId ?? null,
|
|
113
|
+
parentSpanId: ctx?.parentSpanId ?? null,
|
|
114
|
+
depth: ctx?.depth ?? null,
|
|
115
|
+
},
|
|
116
|
+
configurable: true,
|
|
117
|
+
writable: true,
|
|
118
|
+
enumerable: false,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch { }
|
|
123
|
+
const p = origExec.apply(this, args);
|
|
124
|
+
try {
|
|
125
|
+
if (p && typeof p.then === 'function') {
|
|
126
|
+
this.__repro_result_promise = p;
|
|
127
|
+
p.then((res) => {
|
|
128
|
+
try {
|
|
129
|
+
this.__repro_result = res;
|
|
130
|
+
}
|
|
131
|
+
catch { }
|
|
132
|
+
flushQueryFinalizers(this, res, false, null);
|
|
133
|
+
return res;
|
|
134
|
+
}, (err) => {
|
|
135
|
+
flushQueryFinalizers(this, undefined, true, err);
|
|
136
|
+
return err;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch { }
|
|
141
|
+
return p;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch { }
|
|
147
|
+
// Patch Aggregate.prototype.exec as well so aggregation pipelines surface their results in traces.
|
|
148
|
+
try {
|
|
149
|
+
const Ap = targetMongoose?.Aggregate?.prototype;
|
|
150
|
+
if (Ap && !Ap.__repro_agg_exec_patched) {
|
|
151
|
+
const origAggExec = Ap.exec;
|
|
152
|
+
if (typeof origAggExec === 'function') {
|
|
153
|
+
Ap.__repro_agg_exec_patched = true;
|
|
154
|
+
Ap.exec = function reproPatchedAggExec(...args) {
|
|
155
|
+
try {
|
|
156
|
+
this.__repro_is_query = true;
|
|
157
|
+
const ctx = __TRACER__?.getCurrentSpanContext?.();
|
|
158
|
+
const traceId = ctx?.traceId ?? __TRACER__?.getCurrentTraceId?.() ?? null;
|
|
159
|
+
if (ctx || traceId) {
|
|
160
|
+
Object.defineProperty(this, '__repro_span_context', {
|
|
161
|
+
value: {
|
|
162
|
+
traceId,
|
|
163
|
+
spanId: ctx?.spanId ?? null,
|
|
164
|
+
parentSpanId: ctx?.parentSpanId ?? null,
|
|
165
|
+
depth: ctx?.depth ?? null,
|
|
166
|
+
},
|
|
167
|
+
configurable: true,
|
|
168
|
+
writable: true,
|
|
169
|
+
enumerable: false,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch { }
|
|
174
|
+
const p = origAggExec.apply(this, args);
|
|
175
|
+
try {
|
|
176
|
+
if (p && typeof p.then === 'function') {
|
|
177
|
+
this.__repro_result_promise = p;
|
|
178
|
+
p.then((res) => {
|
|
179
|
+
try {
|
|
180
|
+
this.__repro_result = res;
|
|
181
|
+
}
|
|
182
|
+
catch { }
|
|
183
|
+
flushQueryFinalizers(this, res, false, null);
|
|
184
|
+
return res;
|
|
185
|
+
}, (err) => {
|
|
186
|
+
flushQueryFinalizers(this, undefined, true, err);
|
|
187
|
+
return err;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch { }
|
|
192
|
+
return p;
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch { }
|
|
198
|
+
}
|
|
199
|
+
function patchAllKnownMongooseInstances() {
|
|
200
|
+
// Patch the SDK's bundled mongoose first.
|
|
201
|
+
patchMongooseExecCapture(mongoose);
|
|
202
|
+
// Also patch any other mongoose copies the host app might have installed (e.g., different versions).
|
|
203
|
+
try {
|
|
204
|
+
const cache = require?.cache || {};
|
|
205
|
+
const seen = new Set();
|
|
206
|
+
Object.keys(cache).forEach((key) => {
|
|
207
|
+
if (!/node_modules[\\/](mongoose)[\\/]/i.test(key))
|
|
208
|
+
return;
|
|
209
|
+
const mod = cache[key];
|
|
210
|
+
const exp = mod?.exports;
|
|
211
|
+
if (!exp || seen.has(exp))
|
|
212
|
+
return;
|
|
213
|
+
seen.add(exp);
|
|
214
|
+
patchMongooseExecCapture(exp);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
catch { }
|
|
218
|
+
}
|
|
219
|
+
const REQUEST_START_HEADER = 'x-bug-request-start';
|
|
220
|
+
function escapeRx(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
221
|
+
let __TRACER__ = null;
|
|
222
|
+
let __TRACER_READY = false;
|
|
223
|
+
const DEFAULT_INTERCEPTOR_TRACE_RULES = [
|
|
224
|
+
{ fn: /switchToHttp$/i },
|
|
225
|
+
{ fn: /intercept$/i },
|
|
226
|
+
];
|
|
227
|
+
let interceptorTracingEnabled = false;
|
|
228
|
+
let userDisabledFunctionTraceRules = null;
|
|
229
|
+
function computeDisabledFunctionTraceRules(rules) {
|
|
230
|
+
const normalized = Array.isArray(rules)
|
|
231
|
+
? rules.filter((rule) => !!rule)
|
|
232
|
+
: [];
|
|
233
|
+
if (interceptorTracingEnabled) {
|
|
234
|
+
return normalized;
|
|
235
|
+
}
|
|
236
|
+
return [...DEFAULT_INTERCEPTOR_TRACE_RULES, ...normalized];
|
|
237
|
+
}
|
|
238
|
+
function refreshDisabledFunctionTraceRules() {
|
|
239
|
+
disabledFunctionTraceRules = computeDisabledFunctionTraceRules(userDisabledFunctionTraceRules);
|
|
240
|
+
}
|
|
241
|
+
let disabledFunctionTraceRules = computeDisabledFunctionTraceRules();
|
|
242
|
+
let disabledFunctionTypePatterns = [];
|
|
243
|
+
let disabledTraceFilePatterns = [];
|
|
244
|
+
let __TRACE_LOG_PREF = null;
|
|
245
|
+
function setInterceptorTracingEnabled(enabled) {
|
|
246
|
+
const next = !!enabled;
|
|
247
|
+
if (interceptorTracingEnabled === next)
|
|
248
|
+
return;
|
|
249
|
+
interceptorTracingEnabled = next;
|
|
250
|
+
refreshDisabledFunctionTraceRules();
|
|
251
|
+
}
|
|
252
|
+
function hasOwn(obj, key) {
|
|
253
|
+
return !!obj && Object.prototype.hasOwnProperty.call(obj, key);
|
|
254
|
+
}
|
|
255
|
+
function normalizePatternArray(pattern) {
|
|
256
|
+
if (pattern === undefined || pattern === null)
|
|
257
|
+
return [];
|
|
258
|
+
const arr = Array.isArray(pattern) ? pattern : [pattern];
|
|
259
|
+
return arr.filter((entry) => entry !== undefined && entry !== null);
|
|
260
|
+
}
|
|
261
|
+
function matchesPattern(value, pattern, defaultWhenEmpty = true) {
|
|
262
|
+
if (pattern === undefined || pattern === null)
|
|
263
|
+
return defaultWhenEmpty;
|
|
264
|
+
const val = value == null ? '' : String(value);
|
|
265
|
+
const candidates = normalizePatternArray(pattern);
|
|
266
|
+
if (!candidates.length)
|
|
267
|
+
return defaultWhenEmpty;
|
|
268
|
+
return candidates.some(entry => {
|
|
269
|
+
if (entry instanceof RegExp) {
|
|
270
|
+
try {
|
|
271
|
+
return entry.test(val);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const needle = String(entry).toLowerCase();
|
|
278
|
+
if (!needle)
|
|
279
|
+
return val === '';
|
|
280
|
+
return val.toLowerCase().includes(needle);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
function inferLibraryNameFromFile(file) {
|
|
284
|
+
if (!file)
|
|
285
|
+
return null;
|
|
286
|
+
const normalized = String(file).replace(/\\/g, '/');
|
|
287
|
+
const idx = normalized.lastIndexOf('/node_modules/');
|
|
288
|
+
if (idx === -1)
|
|
289
|
+
return null;
|
|
290
|
+
const remainder = normalized.slice(idx + '/node_modules/'.length);
|
|
291
|
+
if (!remainder)
|
|
292
|
+
return null;
|
|
293
|
+
const segments = remainder.split('/');
|
|
294
|
+
if (!segments.length)
|
|
295
|
+
return null;
|
|
296
|
+
if (segments[0].startsWith('@') && segments.length >= 2) {
|
|
297
|
+
return `${segments[0]}/${segments[1]}`;
|
|
298
|
+
}
|
|
299
|
+
return segments[0] || null;
|
|
300
|
+
}
|
|
301
|
+
function inferWrapperClassFromFn(fn) {
|
|
302
|
+
if (!fn)
|
|
303
|
+
return null;
|
|
304
|
+
const raw = String(fn);
|
|
305
|
+
if (!raw)
|
|
306
|
+
return null;
|
|
307
|
+
const hashIdx = raw.indexOf('#');
|
|
308
|
+
if (hashIdx > 0) {
|
|
309
|
+
const left = raw.slice(0, hashIdx).trim();
|
|
310
|
+
return left || null;
|
|
311
|
+
}
|
|
312
|
+
const dotIdx = raw.lastIndexOf('.');
|
|
313
|
+
if (dotIdx > 0) {
|
|
314
|
+
const left = raw.slice(0, dotIdx).trim();
|
|
315
|
+
return left || null;
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
function matchesRule(rule, event) {
|
|
320
|
+
const namePattern = rule.fn ?? rule.functionName;
|
|
321
|
+
if (!matchesPattern(event.fn, namePattern))
|
|
322
|
+
return false;
|
|
323
|
+
const wrapperPattern = rule.wrapper ?? rule.wrapperClass ?? rule.className ?? rule.owner;
|
|
324
|
+
if (!matchesPattern(event.wrapperClass, wrapperPattern))
|
|
325
|
+
return false;
|
|
326
|
+
if (!matchesPattern(event.file, rule.file))
|
|
327
|
+
return false;
|
|
328
|
+
if (!matchesPattern(event.line == null ? null : String(event.line), rule.line))
|
|
329
|
+
return false;
|
|
330
|
+
const libPattern = rule.lib ?? rule.library;
|
|
331
|
+
if (!matchesPattern(event.library, libPattern))
|
|
332
|
+
return false;
|
|
333
|
+
const fnTypePattern = rule.functionType ?? rule.type;
|
|
334
|
+
if (!matchesPattern(event.functionType, fnTypePattern))
|
|
335
|
+
return false;
|
|
336
|
+
const eventTypePattern = rule.eventType ?? rule.event;
|
|
337
|
+
if (!matchesPattern(event.eventType, eventTypePattern))
|
|
338
|
+
return false;
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
function shouldDropTraceEvent(event) {
|
|
342
|
+
if (disabledTraceFilePatterns.length) {
|
|
343
|
+
if (matchesPattern(event.file, disabledTraceFilePatterns, false)) {
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (disabledFunctionTypePatterns.length) {
|
|
348
|
+
if (matchesPattern(event.functionType, disabledFunctionTypePatterns, false)) {
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (!disabledFunctionTraceRules.length)
|
|
353
|
+
return false;
|
|
354
|
+
for (const rule of disabledFunctionTraceRules) {
|
|
355
|
+
try {
|
|
356
|
+
if (typeof rule === 'function') {
|
|
357
|
+
if (rule(event))
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
else if (matchesRule(rule, event)) {
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// ignore user filter errors
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
function shouldPropagateUserDisabledFunctionTrace(event) {
|
|
371
|
+
const rules = userDisabledFunctionTraceRules;
|
|
372
|
+
if (!rules || rules.length === 0)
|
|
373
|
+
return false;
|
|
374
|
+
for (const rule of rules) {
|
|
375
|
+
try {
|
|
376
|
+
if (typeof rule === 'function') {
|
|
377
|
+
const enterEvent = { ...event, type: 'enter', eventType: 'enter' };
|
|
378
|
+
const exitEvent = { ...event, type: 'exit', eventType: 'exit' };
|
|
379
|
+
if (rule(enterEvent) && rule(exitEvent))
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
else if (matchesRule(rule, event)) {
|
|
383
|
+
const eventTypePattern = rule.eventType ?? rule.event;
|
|
384
|
+
if (!matchesPattern('enter', eventTypePattern))
|
|
385
|
+
continue;
|
|
386
|
+
if (!matchesPattern('exit', eventTypePattern))
|
|
387
|
+
continue;
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
// ignore user filter errors
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
function setDisabledFunctionTraces(rules) {
|
|
398
|
+
if (!rules || !Array.isArray(rules)) {
|
|
399
|
+
userDisabledFunctionTraceRules = null;
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
userDisabledFunctionTraceRules = rules.filter((rule) => !!rule);
|
|
403
|
+
}
|
|
404
|
+
refreshDisabledFunctionTraceRules();
|
|
405
|
+
}
|
|
406
|
+
exports.setDisabledFunctionTraces = setDisabledFunctionTraces;
|
|
407
|
+
function setDisabledFunctionTypes(patterns) {
|
|
408
|
+
disabledFunctionTypePatterns = normalizePatternArray(patterns);
|
|
409
|
+
}
|
|
410
|
+
exports.setDisabledFunctionTypes = setDisabledFunctionTypes;
|
|
411
|
+
function flattenTraceFilePatterns(config) {
|
|
412
|
+
if (config === null || config === undefined)
|
|
413
|
+
return [];
|
|
414
|
+
if (Array.isArray(config)) {
|
|
415
|
+
return config.flatMap(entry => flattenTraceFilePatterns(entry));
|
|
416
|
+
}
|
|
417
|
+
if (config instanceof RegExp || typeof config === 'string' || typeof config === 'number') {
|
|
418
|
+
return [config];
|
|
419
|
+
}
|
|
420
|
+
if (typeof config === 'object' && 'file' in config) {
|
|
421
|
+
return normalizePatternArray(config.file);
|
|
422
|
+
}
|
|
423
|
+
return [];
|
|
424
|
+
}
|
|
425
|
+
function setDisabledTraceFiles(config) {
|
|
426
|
+
if (config === null || config === undefined) {
|
|
427
|
+
disabledTraceFilePatterns = [];
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const items = Array.isArray(config) ? config : [config];
|
|
431
|
+
disabledTraceFilePatterns = items.flatMap(entry => flattenTraceFilePatterns(entry)).filter(Boolean);
|
|
432
|
+
}
|
|
433
|
+
exports.setDisabledTraceFiles = setDisabledTraceFiles;
|
|
434
|
+
function applyTraceLogPreference(tracer) {
|
|
435
|
+
if (__TRACE_LOG_PREF === null)
|
|
436
|
+
return;
|
|
437
|
+
try {
|
|
438
|
+
tracer?.setFunctionLogsEnabled?.(__TRACE_LOG_PREF);
|
|
439
|
+
}
|
|
440
|
+
catch { }
|
|
441
|
+
}
|
|
442
|
+
function setReproTraceLogsEnabled(enabled) {
|
|
443
|
+
__TRACE_LOG_PREF = !!enabled;
|
|
444
|
+
applyTraceLogPreference(__TRACER__);
|
|
445
|
+
}
|
|
446
|
+
exports.setReproTraceLogsEnabled = setReproTraceLogsEnabled;
|
|
447
|
+
function enableReproTraceLogs() { setReproTraceLogsEnabled(true); }
|
|
448
|
+
exports.enableReproTraceLogs = enableReproTraceLogs;
|
|
449
|
+
function disableReproTraceLogs() { setReproTraceLogsEnabled(false); }
|
|
450
|
+
exports.disableReproTraceLogs = disableReproTraceLogs;
|
|
451
|
+
function summarizeEndpointFromEvents(events) {
|
|
452
|
+
let endpointTrace = null;
|
|
453
|
+
let preferredAppTrace = null;
|
|
454
|
+
let firstAppTrace = null;
|
|
455
|
+
for (const evt of events) {
|
|
456
|
+
if (evt.type !== 'enter')
|
|
457
|
+
continue;
|
|
458
|
+
if (!isLikelyAppFile(evt.file))
|
|
459
|
+
continue;
|
|
460
|
+
const depthOk = evt.depth === undefined || evt.depth <= 6;
|
|
461
|
+
const trace = toEndpointTrace(evt);
|
|
462
|
+
if (!firstAppTrace && depthOk) {
|
|
463
|
+
firstAppTrace = trace;
|
|
464
|
+
}
|
|
465
|
+
if (isLikelyNestControllerFile(evt.file)) {
|
|
466
|
+
endpointTrace = trace;
|
|
467
|
+
}
|
|
468
|
+
else if (depthOk && !preferredAppTrace && !isLikelyNestGuardFile(evt.file)) {
|
|
469
|
+
preferredAppTrace = trace;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return { endpointTrace, preferredAppTrace, firstAppTrace };
|
|
473
|
+
}
|
|
474
|
+
function defaultTracerInitOpts() {
|
|
475
|
+
const cwd = process.cwd().replace(/\\/g, '/');
|
|
476
|
+
const sdkRoot = __dirname.replace(/\\/g, '/');
|
|
477
|
+
const escapeRx = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
478
|
+
const include = [new RegExp('^' + escapeRx(cwd) + '/(?!node_modules/)')];
|
|
479
|
+
// Only skip our own files and Babel internals; repository/service layers stay instrumented.
|
|
480
|
+
const exclude = [
|
|
481
|
+
new RegExp('^' + escapeRx(sdkRoot) + '/'), // never instrument the SDK itself
|
|
482
|
+
/node_modules[\\/]@babel[\\/].*/,
|
|
483
|
+
];
|
|
484
|
+
return {
|
|
485
|
+
instrument: true,
|
|
486
|
+
include,
|
|
487
|
+
exclude,
|
|
488
|
+
mode: process.env.TRACE_MODE || 'trace',
|
|
489
|
+
samplingMs: 10,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
/** Call this from the client app to enable tracing. Safe to call multiple times. */
|
|
493
|
+
function initReproTracing(opts) {
|
|
494
|
+
const options = opts ?? {};
|
|
495
|
+
if (hasOwn(options, 'traceInterceptors')) {
|
|
496
|
+
setInterceptorTracingEnabled(!!options.traceInterceptors);
|
|
497
|
+
}
|
|
498
|
+
if (hasOwn(options, 'disableFunctionTypes')) {
|
|
499
|
+
setDisabledFunctionTypes(options.disableFunctionTypes ?? null);
|
|
500
|
+
}
|
|
501
|
+
if (hasOwn(options, 'disableFunctionTraces')) {
|
|
502
|
+
setDisabledFunctionTraces(options.disableFunctionTraces ?? null);
|
|
503
|
+
}
|
|
504
|
+
if (hasOwn(options, 'disableTraceFiles')) {
|
|
505
|
+
setDisabledTraceFiles(options.disableTraceFiles ?? null);
|
|
506
|
+
}
|
|
507
|
+
if (hasOwn(options, 'logFunctionCalls') && typeof options.logFunctionCalls === 'boolean') {
|
|
508
|
+
setReproTraceLogsEnabled(options.logFunctionCalls);
|
|
509
|
+
}
|
|
510
|
+
if (__TRACER_READY) {
|
|
511
|
+
applyTraceLogPreference(__TRACER__);
|
|
512
|
+
return __TRACER__;
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
const tracerPkg = require('../tracer');
|
|
516
|
+
__TRACER__ = tracerPkg;
|
|
517
|
+
applyTraceLogPreference(tracerPkg);
|
|
518
|
+
const { disableFunctionTraces: _disableFunctionTraces, disableFunctionTypes: _disableFunctionTypes, disableTraceFiles: _disableTraceFiles, logFunctionCalls: _logFunctionCalls, traceInterceptors: _traceInterceptors, ...rest } = options;
|
|
519
|
+
const initOpts = { ...defaultTracerInitOpts(), ...rest };
|
|
520
|
+
tracerPkg.init?.(initOpts);
|
|
521
|
+
tracerPkg.patchHttp?.();
|
|
522
|
+
applyTraceLogPreference(tracerPkg);
|
|
523
|
+
__TRACER_READY = true;
|
|
524
|
+
patchAllKnownMongooseInstances();
|
|
525
|
+
// Patch again on the next tick to catch mongoose copies that load after init (different versions/copies).
|
|
526
|
+
setImmediate(() => patchAllKnownMongooseInstances());
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
__TRACER__ = null; // SDK still works without tracer
|
|
530
|
+
}
|
|
531
|
+
finally {
|
|
532
|
+
// keep Mongoose prototypes pristine in either case
|
|
533
|
+
restoreMongooseIfNeeded();
|
|
534
|
+
setImmediate(restoreMongooseIfNeeded);
|
|
535
|
+
}
|
|
536
|
+
return __TRACER__;
|
|
537
|
+
}
|
|
538
|
+
exports.initReproTracing = initReproTracing;
|
|
539
|
+
/** Optional helper if users want to check it. */
|
|
540
|
+
function isReproTracingEnabled() { return __TRACER_READY; }
|
|
541
|
+
exports.isReproTracingEnabled = isReproTracingEnabled;
|
|
542
|
+
function captureSpanContextFromTracer(source) {
|
|
543
|
+
try {
|
|
544
|
+
// Prefer a preserved store captured at the call-site for thenables (e.g., Mongoose Query),
|
|
545
|
+
// because the tracer intentionally detaches spans before the query is actually executed.
|
|
546
|
+
const promiseStore = source && source[Symbol.for('__repro_promise_store')];
|
|
547
|
+
if (promiseStore) {
|
|
548
|
+
const stack = Array.isArray(promiseStore.__repro_span_stack) ? promiseStore.__repro_span_stack : [];
|
|
549
|
+
const top = stack.length ? stack[stack.length - 1] : null;
|
|
550
|
+
const spanId = top && top.id != null ? top.id : null;
|
|
551
|
+
const parentSpanId = top && top.parentId != null
|
|
552
|
+
? top.parentId
|
|
553
|
+
: (stack.length >= 2 ? (stack[stack.length - 2]?.id ?? null) : null);
|
|
554
|
+
const depth = top && top.depth != null
|
|
555
|
+
? top.depth
|
|
556
|
+
: (typeof promiseStore.depth === 'number'
|
|
557
|
+
? promiseStore.depth
|
|
558
|
+
: (stack.length ? stack.length : null));
|
|
559
|
+
const traceId = promiseStore.traceId ?? null;
|
|
560
|
+
if (traceId || spanId !== null || parentSpanId !== null) {
|
|
561
|
+
return { traceId, spanId, parentSpanId, depth: depth == null ? null : depth };
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const fromSource = source && source.__repro_span_context;
|
|
565
|
+
if (fromSource) {
|
|
566
|
+
return {
|
|
567
|
+
traceId: fromSource.traceId ?? null,
|
|
568
|
+
spanId: fromSource.spanId ?? null,
|
|
569
|
+
parentSpanId: fromSource.parentSpanId ?? null,
|
|
570
|
+
depth: fromSource.depth ?? null,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
const ctx = __TRACER__?.getCurrentSpanContext?.();
|
|
574
|
+
if (ctx) {
|
|
575
|
+
const span = {
|
|
576
|
+
traceId: ctx.traceId ?? __TRACER__?.getCurrentTraceId?.() ?? null,
|
|
577
|
+
spanId: ctx.spanId ?? null,
|
|
578
|
+
parentSpanId: ctx.parentSpanId ?? null,
|
|
579
|
+
depth: ctx.depth ?? null,
|
|
580
|
+
};
|
|
581
|
+
if (span.traceId || span.spanId !== null || span.parentSpanId !== null) {
|
|
582
|
+
return span;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
else if (__TRACER__?.getCurrentTraceId) {
|
|
586
|
+
const traceId = __TRACER__?.getCurrentTraceId?.() ?? null;
|
|
587
|
+
if (traceId) {
|
|
588
|
+
return { traceId, spanId: null, parentSpanId: null, depth: null };
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
catch { }
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
function isExcludedSpanId(spanId) {
|
|
596
|
+
if (spanId === null || spanId === undefined)
|
|
597
|
+
return false;
|
|
598
|
+
try {
|
|
599
|
+
const excluded = getCtx().excludedSpanIds;
|
|
600
|
+
if (!excluded || excluded.size === 0)
|
|
601
|
+
return false;
|
|
602
|
+
return excluded.has(String(spanId));
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function shouldCaptureDbSpan(span) {
|
|
609
|
+
if (!span || span.spanId === null || span.spanId === undefined)
|
|
610
|
+
return false;
|
|
611
|
+
if (isExcludedSpanId(span.spanId))
|
|
612
|
+
return false;
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
function attachSpanContext(target, span) {
|
|
616
|
+
if (!target)
|
|
617
|
+
return target;
|
|
618
|
+
const ctx = span ?? captureSpanContextFromTracer();
|
|
619
|
+
if (ctx) {
|
|
620
|
+
try {
|
|
621
|
+
target.spanContext = ctx;
|
|
622
|
+
}
|
|
623
|
+
catch { }
|
|
624
|
+
}
|
|
625
|
+
return target;
|
|
626
|
+
}
|
|
627
|
+
const als = new async_hooks_1.AsyncLocalStorage();
|
|
628
|
+
const getCtx = () => als.getStore() || {};
|
|
629
|
+
function currentClockSkewMs() {
|
|
630
|
+
const store = als.getStore();
|
|
631
|
+
const skew = store?.clockSkewMs;
|
|
632
|
+
return Number.isFinite(skew) ? Number(skew) : 0;
|
|
633
|
+
}
|
|
634
|
+
function alignTimestamp(ms) {
|
|
635
|
+
if (!Number.isFinite(ms))
|
|
636
|
+
return ms;
|
|
637
|
+
const skew = currentClockSkewMs();
|
|
638
|
+
return Number.isFinite(skew) ? ms + skew : ms;
|
|
639
|
+
}
|
|
640
|
+
function alignedNow() {
|
|
641
|
+
return alignTimestamp(Date.now());
|
|
642
|
+
}
|
|
643
|
+
let __REQUEST_SEQ = 0;
|
|
644
|
+
function nextRequestId(base) {
|
|
645
|
+
const seq = (++__REQUEST_SEQ).toString(36);
|
|
646
|
+
const rand = Math.floor(Math.random() * 1e6).toString(36);
|
|
647
|
+
return `${base}-${seq}${rand}`;
|
|
648
|
+
}
|
|
649
|
+
// Track in-flight requests per session so we can delay flushing until a session is drained.
|
|
650
|
+
const sessionInFlight = new Map();
|
|
651
|
+
const sessionWaiters = new Map();
|
|
652
|
+
function beginSessionRequest(sessionId) {
|
|
653
|
+
if (!sessionId)
|
|
654
|
+
return;
|
|
655
|
+
sessionInFlight.set(sessionId, (sessionInFlight.get(sessionId) || 0) + 1);
|
|
656
|
+
}
|
|
657
|
+
function endSessionRequest(sessionId) {
|
|
658
|
+
if (!sessionId)
|
|
659
|
+
return;
|
|
660
|
+
const next = (sessionInFlight.get(sessionId) || 0) - 1;
|
|
661
|
+
if (next > 0) {
|
|
662
|
+
sessionInFlight.set(sessionId, next);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
sessionInFlight.delete(sessionId);
|
|
666
|
+
const waiters = sessionWaiters.get(sessionId);
|
|
667
|
+
if (waiters && waiters.length) {
|
|
668
|
+
sessionWaiters.delete(sessionId);
|
|
669
|
+
waiters.forEach(fn => { try {
|
|
670
|
+
fn();
|
|
671
|
+
}
|
|
672
|
+
catch { } });
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function waitForSessionDrain(sessionId) {
|
|
676
|
+
if (!sessionId)
|
|
677
|
+
return Promise.resolve();
|
|
678
|
+
const remaining = sessionInFlight.get(sessionId) || 0;
|
|
679
|
+
if (remaining <= 0)
|
|
680
|
+
return Promise.resolve();
|
|
681
|
+
return new Promise(resolve => {
|
|
682
|
+
const list = sessionWaiters.get(sessionId) || [];
|
|
683
|
+
list.push(resolve);
|
|
684
|
+
sessionWaiters.set(sessionId, list);
|
|
685
|
+
if (SESSION_DRAIN_TIMEOUT_MS > 0 && Number.isFinite(SESSION_DRAIN_TIMEOUT_MS)) {
|
|
686
|
+
setTimeout(resolve, SESSION_DRAIN_TIMEOUT_MS);
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
function balanceTraceEvents(events) {
|
|
691
|
+
// No balancing / synthetic exits — preserve raw event stream.
|
|
692
|
+
return Array.isArray(events) ? events : [];
|
|
693
|
+
}
|
|
694
|
+
function sortTraceEventsChronologically(events) {
|
|
695
|
+
return events
|
|
696
|
+
.map((ev, idx) => ({ ev, idx }))
|
|
697
|
+
.sort((a, b) => {
|
|
698
|
+
const ta = typeof a.ev.t === 'number' ? a.ev.t : Number.POSITIVE_INFINITY;
|
|
699
|
+
const tb = typeof b.ev.t === 'number' ? b.ev.t : Number.POSITIVE_INFINITY;
|
|
700
|
+
if (ta !== tb)
|
|
701
|
+
return ta - tb;
|
|
702
|
+
return a.idx - b.idx; // stable fallback
|
|
703
|
+
})
|
|
704
|
+
.map(w => w.ev);
|
|
705
|
+
}
|
|
706
|
+
function computeDepthsFromParentsChronologically(events) {
|
|
707
|
+
const depthBySpan = new Map();
|
|
708
|
+
const normId = (v) => (v === null || v === undefined ? null : String(v));
|
|
709
|
+
return events.map(ev => {
|
|
710
|
+
const out = { ...ev };
|
|
711
|
+
const sid = normId(ev.spanId);
|
|
712
|
+
const pid = normId(ev.parentSpanId);
|
|
713
|
+
if (ev.type === 'enter') {
|
|
714
|
+
const parentDepth = pid ? depthBySpan.get(pid) ?? 0 : 0;
|
|
715
|
+
const depth = Math.max(1, parentDepth + 1);
|
|
716
|
+
out.depth = depth;
|
|
717
|
+
if (sid)
|
|
718
|
+
depthBySpan.set(sid, depth);
|
|
719
|
+
return out;
|
|
720
|
+
}
|
|
721
|
+
if (ev.type === 'exit') {
|
|
722
|
+
if (sid && depthBySpan.has(sid)) {
|
|
723
|
+
out.depth = depthBySpan.get(sid);
|
|
724
|
+
}
|
|
725
|
+
else if (pid && depthBySpan.has(pid)) {
|
|
726
|
+
out.depth = Math.max(1, (depthBySpan.get(pid) ?? 0) + 1);
|
|
727
|
+
}
|
|
728
|
+
else if (typeof ev.depth === 'number') {
|
|
729
|
+
out.depth = ev.depth;
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
out.depth = 1;
|
|
733
|
+
}
|
|
734
|
+
return out;
|
|
735
|
+
}
|
|
736
|
+
return out;
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
function reorderTraceEvents(events) {
|
|
740
|
+
if (!Array.isArray(events) || !events.length)
|
|
741
|
+
return events;
|
|
742
|
+
const nodes = new Map();
|
|
743
|
+
const roots = [];
|
|
744
|
+
const normalizeId = (v) => (v === null || v === undefined ? null : String(v));
|
|
745
|
+
const ensureNode = (id) => {
|
|
746
|
+
let n = nodes.get(id);
|
|
747
|
+
if (!n) {
|
|
748
|
+
n = { id, parentId: null, children: [], order: Number.POSITIVE_INFINITY };
|
|
749
|
+
nodes.set(id, n);
|
|
750
|
+
}
|
|
751
|
+
return n;
|
|
752
|
+
};
|
|
753
|
+
events.forEach((ev, idx) => {
|
|
754
|
+
const spanId = normalizeId(ev.spanId);
|
|
755
|
+
const parentId = normalizeId(ev.parentSpanId);
|
|
756
|
+
if (!spanId) {
|
|
757
|
+
roots.push({ order: idx, ev });
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const node = ensureNode(spanId);
|
|
761
|
+
node.order = Math.min(node.order, idx);
|
|
762
|
+
node.parentId = parentId;
|
|
763
|
+
if (ev.type === 'enter')
|
|
764
|
+
node.enter = node.enter ?? ev;
|
|
765
|
+
if (ev.type === 'exit')
|
|
766
|
+
node.exit = ev;
|
|
767
|
+
});
|
|
768
|
+
nodes.forEach(node => {
|
|
769
|
+
if (node.parentId && nodes.has(node.parentId)) {
|
|
770
|
+
const parent = nodes.get(node.parentId);
|
|
771
|
+
parent.children.push(node);
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
roots.push(node);
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
const sortChildren = (node) => {
|
|
778
|
+
node.children.sort((a, b) => a.order - b.order);
|
|
779
|
+
node.children.forEach(sortChildren);
|
|
780
|
+
};
|
|
781
|
+
nodes.forEach(sortChildren);
|
|
782
|
+
roots.sort((a, b) => a.order - b.order);
|
|
783
|
+
const out = [];
|
|
784
|
+
const emitNode = (node, depth) => {
|
|
785
|
+
if (node.enter) {
|
|
786
|
+
node.enter.depth = depth;
|
|
787
|
+
out.push(node.enter);
|
|
788
|
+
}
|
|
789
|
+
node.children.forEach(child => emitNode(child, depth + 1));
|
|
790
|
+
if (node.exit) {
|
|
791
|
+
node.exit.depth = depth;
|
|
792
|
+
out.push(node.exit);
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
roots.forEach(entry => {
|
|
796
|
+
if ('ev' in entry) {
|
|
797
|
+
out.push(entry.ev);
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
emitNode(entry, 1);
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
return out;
|
|
804
|
+
}
|
|
805
|
+
function getCollectionNameFromDoc(doc) {
|
|
806
|
+
const direct = doc?.$__?.collection?.collectionName ||
|
|
807
|
+
doc?.$collection?.collectionName ||
|
|
808
|
+
doc?.collection?.collectionName ||
|
|
809
|
+
doc?.collection?.name ||
|
|
810
|
+
doc?.constructor?.collection?.collectionName;
|
|
811
|
+
if (direct)
|
|
812
|
+
return direct;
|
|
813
|
+
if (doc?.$isSubdocument && typeof doc.ownerDocument === 'function') {
|
|
814
|
+
const parent = doc.ownerDocument();
|
|
815
|
+
return (parent?.$__?.collection?.collectionName ||
|
|
816
|
+
parent?.$collection?.collectionName ||
|
|
817
|
+
parent?.collection?.collectionName ||
|
|
818
|
+
parent?.collection?.name ||
|
|
819
|
+
parent?.constructor?.collection?.collectionName);
|
|
820
|
+
}
|
|
821
|
+
const ctor = doc?.constructor;
|
|
822
|
+
if (ctor?.base && ctor?.base?.collection?.collectionName) {
|
|
823
|
+
return ctor.base.collection.collectionName;
|
|
824
|
+
}
|
|
825
|
+
return undefined;
|
|
826
|
+
}
|
|
827
|
+
function getCollectionNameFromQuery(q) {
|
|
828
|
+
return q?.model?.collection?.collectionName || q?.model?.collection?.name;
|
|
829
|
+
}
|
|
830
|
+
function resolveCollectionOrWarn(source, type) {
|
|
831
|
+
const name = (type === 'doc'
|
|
832
|
+
? getCollectionNameFromDoc(source)
|
|
833
|
+
: getCollectionNameFromQuery(source)) || undefined;
|
|
834
|
+
if (!name) {
|
|
835
|
+
try {
|
|
836
|
+
const modelName = type === 'doc'
|
|
837
|
+
? source?.constructor?.modelName ||
|
|
838
|
+
source?.ownerDocument?.()?.constructor?.modelName
|
|
839
|
+
: source?.model?.modelName;
|
|
840
|
+
// eslint-disable-next-line no-console
|
|
841
|
+
console.warn('[repro] could not resolve collection name', { type, modelName });
|
|
842
|
+
}
|
|
843
|
+
catch { }
|
|
844
|
+
return 'unknown';
|
|
845
|
+
}
|
|
846
|
+
return name;
|
|
847
|
+
}
|
|
848
|
+
async function post(cfg, sessionId, body) {
|
|
849
|
+
try {
|
|
850
|
+
const envBase = typeof process !== 'undefined' ? process?.env?.REPRO_API_BASE : undefined;
|
|
851
|
+
const legacyBase = cfg?.apiBase;
|
|
852
|
+
const apiBase = String(envBase || legacyBase || 'https://oozy-loreta-gully.ngrok-free.dev').replace(/\/+$/, '');
|
|
853
|
+
await fetch(`${apiBase}/v1/sessions/${sessionId}/backend`, {
|
|
854
|
+
method: 'POST',
|
|
855
|
+
headers: {
|
|
856
|
+
'Content-Type': 'application/json',
|
|
857
|
+
'X-App-Id': cfg.appId,
|
|
858
|
+
'X-App-Secret': cfg.appSecret,
|
|
859
|
+
...(cfg.appName ? { 'X-App-Name': cfg.appName } : {}),
|
|
860
|
+
},
|
|
861
|
+
body: JSON.stringify(body),
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
catch { /* swallow in SDK */ }
|
|
865
|
+
}
|
|
866
|
+
function readHeaderNumber(value) {
|
|
867
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
868
|
+
if (typeof raw !== 'string')
|
|
869
|
+
return null;
|
|
870
|
+
const trimmed = raw.trim();
|
|
871
|
+
if (!trimmed)
|
|
872
|
+
return null;
|
|
873
|
+
const parsed = Number(trimmed);
|
|
874
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
875
|
+
}
|
|
876
|
+
// -------- helpers for response capture & grouping --------
|
|
877
|
+
function normalizeRouteKey(method, rawPath) {
|
|
878
|
+
const base = (rawPath || '/').split('?')[0] || '/';
|
|
879
|
+
return `${String(method || 'GET').toUpperCase()} ${base}`;
|
|
880
|
+
}
|
|
881
|
+
function normalizeFilePath(file) {
|
|
882
|
+
return file ? String(file).replace(/\\/g, '/').toLowerCase() : '';
|
|
883
|
+
}
|
|
884
|
+
function isLikelyAppFile(file) {
|
|
885
|
+
if (!file)
|
|
886
|
+
return false;
|
|
887
|
+
const normalized = String(file).replace(/\\/g, '/');
|
|
888
|
+
if (!normalized)
|
|
889
|
+
return false;
|
|
890
|
+
return !normalized.includes('/node_modules/');
|
|
891
|
+
}
|
|
892
|
+
function isLikelyNestControllerFile(file) {
|
|
893
|
+
const normalized = normalizeFilePath(file);
|
|
894
|
+
if (!normalized)
|
|
895
|
+
return false;
|
|
896
|
+
if (!isLikelyAppFile(file))
|
|
897
|
+
return false;
|
|
898
|
+
return (normalized.includes('.controller.') ||
|
|
899
|
+
normalized.includes('/controllers/') ||
|
|
900
|
+
normalized.includes('.resolver.') ||
|
|
901
|
+
normalized.includes('/resolvers/'));
|
|
902
|
+
}
|
|
903
|
+
function isLikelyNestGuardFile(file) {
|
|
904
|
+
const normalized = normalizeFilePath(file);
|
|
905
|
+
if (!normalized)
|
|
906
|
+
return false;
|
|
907
|
+
return normalized.includes('.guard.') || normalized.includes('/guards/');
|
|
908
|
+
}
|
|
909
|
+
function toEndpointTrace(evt) {
|
|
910
|
+
return {
|
|
911
|
+
fn: evt.fn ?? null,
|
|
912
|
+
file: evt.file ?? null,
|
|
913
|
+
line: evt.line ?? null,
|
|
914
|
+
functionType: evt.functionType ?? null,
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
function coerceBodyToStorable(body, contentType) {
|
|
918
|
+
if (body && typeof body === 'object' && !Buffer.isBuffer(body))
|
|
919
|
+
return body;
|
|
920
|
+
const ct = Array.isArray(contentType) ? String(contentType[0]) : String(contentType || '');
|
|
921
|
+
const isLikelyJson = ct.toLowerCase().includes('application/json');
|
|
922
|
+
try {
|
|
923
|
+
if (Buffer.isBuffer(body)) {
|
|
924
|
+
const s = body.toString('utf8');
|
|
925
|
+
return isLikelyJson ? JSON.parse(s) : s;
|
|
926
|
+
}
|
|
927
|
+
if (typeof body === 'string') {
|
|
928
|
+
return isLikelyJson ? JSON.parse(body) : body;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
catch {
|
|
932
|
+
if (Buffer.isBuffer(body))
|
|
933
|
+
return body.toString('utf8');
|
|
934
|
+
if (typeof body === 'string')
|
|
935
|
+
return body;
|
|
936
|
+
}
|
|
937
|
+
return body;
|
|
938
|
+
}
|
|
939
|
+
const TRACE_VALUE_MAX_DEPTH = 3;
|
|
940
|
+
const TRACE_VALUE_MAX_KEYS = 20;
|
|
941
|
+
const TRACE_VALUE_MAX_ITEMS = 20;
|
|
942
|
+
const TRACE_VALUE_MAX_STRING = 2000;
|
|
943
|
+
const TRACE_BATCH_SIZE = 100;
|
|
944
|
+
const TRACE_FLUSH_DELAY_MS = 20;
|
|
945
|
+
// Choose how to order trace events in payloads.
|
|
946
|
+
// - "chronological" (default): preserve event arrival order (no reshuffle).
|
|
947
|
+
// - "tree": rebuild a parent/child tree from spanIds for depth visualizations (can shuffle concurrent spans).
|
|
948
|
+
const TRACE_ORDER_MODE = (() => {
|
|
949
|
+
const mode = String(process.env.TRACE_ORDER_MODE || '').toLowerCase().trim();
|
|
950
|
+
return mode === 'tree' ? 'tree' : 'chronological';
|
|
951
|
+
})();
|
|
952
|
+
// Extra grace period after res.finish to catch late fire-and-forget work before unsubscribing.
|
|
953
|
+
const TRACE_LINGER_AFTER_FINISH_MS = (() => {
|
|
954
|
+
const env = Number(process.env.TRACE_LINGER_AFTER_FINISH_MS);
|
|
955
|
+
if (Number.isFinite(env) && env >= 0)
|
|
956
|
+
return env;
|
|
957
|
+
return 1000; // default 1s to catch slow async callbacks (e.g., email sends)
|
|
958
|
+
})();
|
|
959
|
+
const TRACE_IDLE_FLUSH_MS = (() => {
|
|
960
|
+
const env = Number(process.env.TRACE_IDLE_FLUSH_MS);
|
|
961
|
+
if (Number.isFinite(env) && env > 0)
|
|
962
|
+
return env;
|
|
963
|
+
// Keep the listener alive until no new events arrive for this many ms after finish.
|
|
964
|
+
return 2000;
|
|
965
|
+
})();
|
|
966
|
+
const SESSION_DRAIN_TIMEOUT_MS = (() => {
|
|
967
|
+
const env = Number(process.env.SESSION_DRAIN_TIMEOUT_MS);
|
|
968
|
+
if (Number.isFinite(env) && env >= 0)
|
|
969
|
+
return env;
|
|
970
|
+
// Bound wait for draining sessions to avoid lost flushes when a request hangs.
|
|
971
|
+
return 10000;
|
|
972
|
+
})();
|
|
973
|
+
function isThenable(value) {
|
|
974
|
+
return value != null && typeof value === 'object' && typeof value.then === 'function';
|
|
975
|
+
}
|
|
976
|
+
function coerceMongoId(value) {
|
|
977
|
+
if (!value)
|
|
978
|
+
return null;
|
|
979
|
+
const name = value?.constructor?.name;
|
|
980
|
+
if (name === 'ObjectId' || name === 'ObjectID' || value?._bsontype === 'ObjectID') {
|
|
981
|
+
try {
|
|
982
|
+
if (typeof value.toHexString === 'function')
|
|
983
|
+
return value.toHexString();
|
|
984
|
+
if (typeof value.toString === 'function')
|
|
985
|
+
return value.toString();
|
|
986
|
+
}
|
|
987
|
+
catch { }
|
|
988
|
+
return '[mongo-id]';
|
|
989
|
+
}
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
function isMongoSessionLike(value) {
|
|
993
|
+
const ctor = value?.constructor?.name?.toLowerCase?.() || '';
|
|
994
|
+
return (!!ctor &&
|
|
995
|
+
ctor.includes('session') &&
|
|
996
|
+
(typeof value.endSession === 'function' || typeof value.inTransaction === 'function'));
|
|
997
|
+
}
|
|
998
|
+
function describeMongoPlaceholder(value) {
|
|
999
|
+
const ctor = value?.constructor?.name;
|
|
1000
|
+
if (!ctor)
|
|
1001
|
+
return null;
|
|
1002
|
+
const lower = ctor.toLowerCase();
|
|
1003
|
+
if (isMongoSessionLike(value))
|
|
1004
|
+
return 'mongo-session';
|
|
1005
|
+
if (lower.includes('cursor'))
|
|
1006
|
+
return 'mongo-cursor';
|
|
1007
|
+
if (lower.includes('topology'))
|
|
1008
|
+
return 'mongo-topology';
|
|
1009
|
+
if (lower.includes('connection'))
|
|
1010
|
+
return 'mongo-connection';
|
|
1011
|
+
if (lower.includes('collection')) {
|
|
1012
|
+
const name = value?.collectionName || value?.name;
|
|
1013
|
+
return name ? `mongo-collection(${name})` : 'mongo-collection';
|
|
1014
|
+
}
|
|
1015
|
+
if (lower.includes('db') && typeof value.command === 'function')
|
|
1016
|
+
return 'mongo-db';
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
function isMongooseDocumentLike(value) {
|
|
1020
|
+
return !!value && typeof value === 'object' && typeof value.toObject === 'function' && !!value.$__;
|
|
1021
|
+
}
|
|
1022
|
+
function toPlainMongooseDoc(value) {
|
|
1023
|
+
try {
|
|
1024
|
+
const plain = value.toObject?.({ depopulate: true, virtuals: false, minimize: false, getters: false });
|
|
1025
|
+
if (plain && plain !== value)
|
|
1026
|
+
return plain;
|
|
1027
|
+
}
|
|
1028
|
+
catch { }
|
|
1029
|
+
try {
|
|
1030
|
+
const json = value.toJSON?.();
|
|
1031
|
+
if (json && json !== value)
|
|
1032
|
+
return json;
|
|
1033
|
+
}
|
|
1034
|
+
catch { }
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1037
|
+
function isMongooseQueryLike(value) {
|
|
1038
|
+
return !!value && typeof value === 'object' && typeof value.exec === 'function' && (value.model || value.op);
|
|
1039
|
+
}
|
|
1040
|
+
function summarizeMongooseQueryValue(value, depth, seen) {
|
|
1041
|
+
try {
|
|
1042
|
+
const model = value.model?.modelName || value._model?.modelName || undefined;
|
|
1043
|
+
const op = value.op || value.operation || value.options?.op || undefined;
|
|
1044
|
+
return {
|
|
1045
|
+
__type: 'MongooseQuery',
|
|
1046
|
+
model,
|
|
1047
|
+
op,
|
|
1048
|
+
filter: sanitizeTraceValue(value.getFilter?.() ?? value._conditions, depth + 1, seen),
|
|
1049
|
+
update: sanitizeTraceValue(value.getUpdate?.() ?? value._update, depth + 1, seen),
|
|
1050
|
+
options: sanitizeTraceValue(value.getOptions?.() ?? value.options, depth + 1, seen),
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
catch {
|
|
1054
|
+
return 'mongo-query';
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
function safeStringifyUnknown(value) {
|
|
1058
|
+
try {
|
|
1059
|
+
const str = String(value);
|
|
1060
|
+
if (str === '[object Object]')
|
|
1061
|
+
return '[unserializable]';
|
|
1062
|
+
return str;
|
|
1063
|
+
}
|
|
1064
|
+
catch {
|
|
1065
|
+
return undefined;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
function chunkArray(arr, size) {
|
|
1069
|
+
if (!Array.isArray(arr) || arr.length === 0)
|
|
1070
|
+
return [];
|
|
1071
|
+
if (!size || size <= 0)
|
|
1072
|
+
return [arr.slice()];
|
|
1073
|
+
const batches = [];
|
|
1074
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
1075
|
+
batches.push(arr.slice(i, i + size));
|
|
1076
|
+
}
|
|
1077
|
+
return batches;
|
|
1078
|
+
}
|
|
1079
|
+
function sanitizeTraceValue(value, depth = 0, seen = new WeakSet()) {
|
|
1080
|
+
if (value === null || value === undefined)
|
|
1081
|
+
return value;
|
|
1082
|
+
const type = typeof value;
|
|
1083
|
+
if (type === 'number' || type === 'boolean')
|
|
1084
|
+
return value;
|
|
1085
|
+
if (type === 'string') {
|
|
1086
|
+
if (value.length <= TRACE_VALUE_MAX_STRING)
|
|
1087
|
+
return value;
|
|
1088
|
+
return `${value.slice(0, TRACE_VALUE_MAX_STRING)}…(${value.length - TRACE_VALUE_MAX_STRING} more chars)`;
|
|
1089
|
+
}
|
|
1090
|
+
if (type === 'bigint')
|
|
1091
|
+
return value.toString();
|
|
1092
|
+
if (type === 'symbol')
|
|
1093
|
+
return value.toString();
|
|
1094
|
+
if (type === 'function')
|
|
1095
|
+
return `[Function${value.name ? ` ${value.name}` : ''}]`;
|
|
1096
|
+
const mongoId = coerceMongoId(value);
|
|
1097
|
+
if (mongoId !== null)
|
|
1098
|
+
return mongoId;
|
|
1099
|
+
if (isMongooseQueryLike(value)) {
|
|
1100
|
+
const captured = value.__repro_result;
|
|
1101
|
+
if (captured !== undefined) {
|
|
1102
|
+
return sanitizeTraceValue(captured, depth + 1, seen);
|
|
1103
|
+
}
|
|
1104
|
+
return summarizeMongooseQueryValue(value, depth, seen);
|
|
1105
|
+
}
|
|
1106
|
+
const mongoPlaceholder = describeMongoPlaceholder(value);
|
|
1107
|
+
if (mongoPlaceholder)
|
|
1108
|
+
return mongoPlaceholder;
|
|
1109
|
+
if (isThenable(value)) {
|
|
1110
|
+
return { __type: 'Promise', state: 'pending' };
|
|
1111
|
+
}
|
|
1112
|
+
if (isMongooseDocumentLike(value)) {
|
|
1113
|
+
const plain = toPlainMongooseDoc(value);
|
|
1114
|
+
if (plain && plain !== value) {
|
|
1115
|
+
return sanitizeTraceValue(plain, depth, seen);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
if (Buffer.isBuffer(value)) {
|
|
1119
|
+
return {
|
|
1120
|
+
__type: 'Buffer',
|
|
1121
|
+
length: value.length,
|
|
1122
|
+
preview: value.length ? value.slice(0, 32).toString('hex') : '',
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
if (value instanceof Date)
|
|
1126
|
+
return value.toISOString();
|
|
1127
|
+
if (value instanceof RegExp)
|
|
1128
|
+
return value.toString();
|
|
1129
|
+
if (value instanceof Error) {
|
|
1130
|
+
return {
|
|
1131
|
+
name: value.name,
|
|
1132
|
+
message: value.message,
|
|
1133
|
+
stack: value.stack,
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
if (type !== 'object')
|
|
1137
|
+
return String(value);
|
|
1138
|
+
if (!Array.isArray(value) && !(value instanceof Map) && !(value instanceof Set)) {
|
|
1139
|
+
const dehydrated = dehydrateComplexValue(value);
|
|
1140
|
+
if (dehydrated !== value) {
|
|
1141
|
+
return sanitizeTraceValue(dehydrated, depth, seen);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
if (seen.has(value))
|
|
1145
|
+
return '[Circular]';
|
|
1146
|
+
seen.add(value);
|
|
1147
|
+
if (depth >= TRACE_VALUE_MAX_DEPTH) {
|
|
1148
|
+
const shallow = safeJson(value);
|
|
1149
|
+
if (shallow !== undefined) {
|
|
1150
|
+
return shallow;
|
|
1151
|
+
}
|
|
1152
|
+
const ctor = value?.constructor?.name;
|
|
1153
|
+
return ctor && ctor !== 'Object'
|
|
1154
|
+
? { __class: ctor, __truncated: `depth>${TRACE_VALUE_MAX_DEPTH}` }
|
|
1155
|
+
: { __truncated: `depth>${TRACE_VALUE_MAX_DEPTH}` };
|
|
1156
|
+
}
|
|
1157
|
+
if (Array.isArray(value)) {
|
|
1158
|
+
const out = value.slice(0, TRACE_VALUE_MAX_ITEMS)
|
|
1159
|
+
.map(item => sanitizeTraceValue(item, depth + 1, seen));
|
|
1160
|
+
if (value.length > TRACE_VALUE_MAX_ITEMS) {
|
|
1161
|
+
out.push(`…(${value.length - TRACE_VALUE_MAX_ITEMS} more items)`);
|
|
1162
|
+
}
|
|
1163
|
+
return out;
|
|
1164
|
+
}
|
|
1165
|
+
if (value instanceof Map) {
|
|
1166
|
+
const entries = [];
|
|
1167
|
+
for (const [k, v] of value) {
|
|
1168
|
+
if (entries.length >= TRACE_VALUE_MAX_ITEMS)
|
|
1169
|
+
break;
|
|
1170
|
+
entries.push([
|
|
1171
|
+
sanitizeTraceValue(k, depth + 1, seen),
|
|
1172
|
+
sanitizeTraceValue(v, depth + 1, seen)
|
|
1173
|
+
]);
|
|
1174
|
+
}
|
|
1175
|
+
if (value.size > TRACE_VALUE_MAX_ITEMS) {
|
|
1176
|
+
entries.push([`…(${value.size - TRACE_VALUE_MAX_ITEMS} more entries)`, null]);
|
|
1177
|
+
}
|
|
1178
|
+
return { __type: 'Map', entries };
|
|
1179
|
+
}
|
|
1180
|
+
if (value instanceof Set) {
|
|
1181
|
+
const arr = [];
|
|
1182
|
+
for (const item of value) {
|
|
1183
|
+
if (arr.length >= TRACE_VALUE_MAX_ITEMS)
|
|
1184
|
+
break;
|
|
1185
|
+
arr.push(sanitizeTraceValue(item, depth + 1, seen));
|
|
1186
|
+
}
|
|
1187
|
+
if (value.size > TRACE_VALUE_MAX_ITEMS) {
|
|
1188
|
+
arr.push(`…(${value.size - TRACE_VALUE_MAX_ITEMS} more items)`);
|
|
1189
|
+
}
|
|
1190
|
+
return { __type: 'Set', values: arr };
|
|
1191
|
+
}
|
|
1192
|
+
const ctor = value?.constructor?.name;
|
|
1193
|
+
const result = {};
|
|
1194
|
+
const keys = Object.keys(value);
|
|
1195
|
+
for (const key of keys.slice(0, TRACE_VALUE_MAX_KEYS)) {
|
|
1196
|
+
try {
|
|
1197
|
+
result[key] = sanitizeTraceValue(value[key], depth + 1, seen);
|
|
1198
|
+
}
|
|
1199
|
+
catch (err) {
|
|
1200
|
+
result[key] = `[Cannot serialize: ${err?.message || 'unknown error'}]`;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
if (keys.length > TRACE_VALUE_MAX_KEYS) {
|
|
1204
|
+
result.__truncatedKeys = keys.length - TRACE_VALUE_MAX_KEYS;
|
|
1205
|
+
}
|
|
1206
|
+
if (ctor && ctor !== 'Object') {
|
|
1207
|
+
result.__class = ctor;
|
|
1208
|
+
}
|
|
1209
|
+
return result;
|
|
1210
|
+
}
|
|
1211
|
+
function sanitizeTraceArgs(values) {
|
|
1212
|
+
if (!Array.isArray(values))
|
|
1213
|
+
return values;
|
|
1214
|
+
return values.map(v => sanitizeTraceValue(v));
|
|
1215
|
+
}
|
|
1216
|
+
function sanitizeRequestSnapshot(value) {
|
|
1217
|
+
if (value === undefined)
|
|
1218
|
+
return undefined;
|
|
1219
|
+
try {
|
|
1220
|
+
return sanitizeTraceValue(value);
|
|
1221
|
+
}
|
|
1222
|
+
catch {
|
|
1223
|
+
if (value === null)
|
|
1224
|
+
return null;
|
|
1225
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
1226
|
+
return value;
|
|
1227
|
+
}
|
|
1228
|
+
try {
|
|
1229
|
+
return JSON.parse(JSON.stringify(value));
|
|
1230
|
+
}
|
|
1231
|
+
catch { }
|
|
1232
|
+
return safeStringifyUnknown(value);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
const DEFAULT_SENSITIVE_HEADERS = [
|
|
1236
|
+
'authorization',
|
|
1237
|
+
'proxy-authorization',
|
|
1238
|
+
'authentication',
|
|
1239
|
+
'auth',
|
|
1240
|
+
'x-api-key',
|
|
1241
|
+
'api-key',
|
|
1242
|
+
'apikey',
|
|
1243
|
+
'x-api-token',
|
|
1244
|
+
'x-access-token',
|
|
1245
|
+
'x-auth-token',
|
|
1246
|
+
'x-id-token',
|
|
1247
|
+
'x-refresh-token',
|
|
1248
|
+
'id-token',
|
|
1249
|
+
'refresh-token',
|
|
1250
|
+
'cookie',
|
|
1251
|
+
'set-cookie',
|
|
1252
|
+
];
|
|
1253
|
+
const DEFAULT_MASK_REPLACEMENT = '[REDACTED]';
|
|
1254
|
+
function normalizeHeaderRules(rules) {
|
|
1255
|
+
return normalizePatternArray(rules || []);
|
|
1256
|
+
}
|
|
1257
|
+
function matchesHeaderRule(name, rules) {
|
|
1258
|
+
if (!rules || !rules.length)
|
|
1259
|
+
return false;
|
|
1260
|
+
const lower = name.toLowerCase();
|
|
1261
|
+
return rules.some(rule => {
|
|
1262
|
+
if (rule instanceof RegExp) {
|
|
1263
|
+
try {
|
|
1264
|
+
return rule.test(name);
|
|
1265
|
+
}
|
|
1266
|
+
catch {
|
|
1267
|
+
return false;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
return lower === String(rule).toLowerCase();
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
function normalizeHeaderCaptureConfig(raw) {
|
|
1274
|
+
if (raw === false) {
|
|
1275
|
+
return { enabled: false, allowSensitive: false, mask: [], unmask: [] };
|
|
1276
|
+
}
|
|
1277
|
+
const opts = raw && raw !== true ? raw : {};
|
|
1278
|
+
return {
|
|
1279
|
+
enabled: true,
|
|
1280
|
+
allowSensitive: opts.allowSensitiveHeaders === true,
|
|
1281
|
+
mask: [...normalizeHeaderRules(opts.maskHeaders), ...normalizeHeaderRules(opts.dropHeaders)],
|
|
1282
|
+
unmask: [...normalizeHeaderRules(opts.unmaskHeaders), ...normalizeHeaderRules(opts.keepHeaders)],
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
function sanitizeHeaderValue(value) {
|
|
1286
|
+
if (value === undefined)
|
|
1287
|
+
return undefined;
|
|
1288
|
+
if (Array.isArray(value)) {
|
|
1289
|
+
const arr = value.map(v => sanitizeHeaderValue(v)).filter(v => v !== undefined);
|
|
1290
|
+
return arr.length ? arr : undefined;
|
|
1291
|
+
}
|
|
1292
|
+
if (value === null)
|
|
1293
|
+
return null;
|
|
1294
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
|
|
1295
|
+
return value;
|
|
1296
|
+
const safe = safeJson(value);
|
|
1297
|
+
if (safe !== undefined)
|
|
1298
|
+
return safe;
|
|
1299
|
+
return safeStringifyUnknown(value);
|
|
1300
|
+
}
|
|
1301
|
+
function sanitizeHeaders(headers, rawCfg) {
|
|
1302
|
+
const cfg = normalizeHeaderCaptureConfig(rawCfg);
|
|
1303
|
+
if (!cfg.enabled)
|
|
1304
|
+
return {};
|
|
1305
|
+
const out = {};
|
|
1306
|
+
for (const [rawKey, rawVal] of Object.entries(headers || {})) {
|
|
1307
|
+
const key = String(rawKey || '').toLowerCase();
|
|
1308
|
+
if (!key)
|
|
1309
|
+
continue;
|
|
1310
|
+
const sanitizedValue = sanitizeHeaderValue(rawVal);
|
|
1311
|
+
if (sanitizedValue !== undefined) {
|
|
1312
|
+
out[key] = sanitizedValue;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
const maskList = cfg.allowSensitive ? cfg.mask : [...DEFAULT_SENSITIVE_HEADERS, ...cfg.mask];
|
|
1316
|
+
Object.keys(out).forEach((key) => {
|
|
1317
|
+
if (!matchesHeaderRule(key, maskList))
|
|
1318
|
+
return;
|
|
1319
|
+
if (matchesHeaderRule(key, cfg.unmask))
|
|
1320
|
+
return;
|
|
1321
|
+
const current = out[key];
|
|
1322
|
+
if (Array.isArray(current)) {
|
|
1323
|
+
out[key] = current.map(() => DEFAULT_MASK_REPLACEMENT);
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
out[key] = DEFAULT_MASK_REPLACEMENT;
|
|
1327
|
+
});
|
|
1328
|
+
return out;
|
|
1329
|
+
}
|
|
1330
|
+
function normalizeMaskTargets(target) {
|
|
1331
|
+
const out = Array.isArray(target) ? target : [target];
|
|
1332
|
+
return out.filter((t) => typeof t === 'string' && t.length > 0);
|
|
1333
|
+
}
|
|
1334
|
+
function parseMaskPath(raw) {
|
|
1335
|
+
if (!raw)
|
|
1336
|
+
return [];
|
|
1337
|
+
let path = String(raw).trim();
|
|
1338
|
+
if (!path)
|
|
1339
|
+
return [];
|
|
1340
|
+
if (path.startsWith('$.'))
|
|
1341
|
+
path = path.slice(2);
|
|
1342
|
+
if (path.startsWith('.'))
|
|
1343
|
+
path = path.slice(1);
|
|
1344
|
+
path = path.replace(/\[(\d+|\*)\]/g, '.$1');
|
|
1345
|
+
return path.split('.').map(s => s.trim()).filter(Boolean);
|
|
1346
|
+
}
|
|
1347
|
+
function normalizeMaskPaths(paths) {
|
|
1348
|
+
if (!paths)
|
|
1349
|
+
return [];
|
|
1350
|
+
const list = Array.isArray(paths) ? paths : [paths];
|
|
1351
|
+
return list
|
|
1352
|
+
.map(p => parseMaskPath(p))
|
|
1353
|
+
.filter(parts => parts.length > 0);
|
|
1354
|
+
}
|
|
1355
|
+
function normalizeMaskingConfig(raw) {
|
|
1356
|
+
const rules = raw?.rules;
|
|
1357
|
+
if (!Array.isArray(rules) || rules.length === 0)
|
|
1358
|
+
return null;
|
|
1359
|
+
const replacement = raw?.replacement ?? DEFAULT_MASK_REPLACEMENT;
|
|
1360
|
+
const normalized = [];
|
|
1361
|
+
for (const rule of rules) {
|
|
1362
|
+
if (!rule)
|
|
1363
|
+
continue;
|
|
1364
|
+
const targets = normalizeMaskTargets(rule.target);
|
|
1365
|
+
if (!targets.length)
|
|
1366
|
+
continue;
|
|
1367
|
+
const paths = normalizeMaskPaths(rule.paths);
|
|
1368
|
+
const keys = rule.keys ?? undefined;
|
|
1369
|
+
if (!paths.length && !keys)
|
|
1370
|
+
continue;
|
|
1371
|
+
normalized.push({
|
|
1372
|
+
when: rule.when,
|
|
1373
|
+
targets,
|
|
1374
|
+
paths,
|
|
1375
|
+
keys,
|
|
1376
|
+
replacement: rule.replacement ?? replacement,
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
return normalized.length ? { replacement, rules: normalized } : null;
|
|
1380
|
+
}
|
|
1381
|
+
function maskWhenRequiresTrace(when) {
|
|
1382
|
+
return Boolean(when.fn ||
|
|
1383
|
+
when.functionName ||
|
|
1384
|
+
when.wrapper ||
|
|
1385
|
+
when.wrapperClass ||
|
|
1386
|
+
when.className ||
|
|
1387
|
+
when.owner ||
|
|
1388
|
+
when.file ||
|
|
1389
|
+
when.lib ||
|
|
1390
|
+
when.library ||
|
|
1391
|
+
when.type ||
|
|
1392
|
+
when.functionType ||
|
|
1393
|
+
when.event ||
|
|
1394
|
+
when.eventType);
|
|
1395
|
+
}
|
|
1396
|
+
function matchesMaskWhen(when, req, trace) {
|
|
1397
|
+
if (!when)
|
|
1398
|
+
return true;
|
|
1399
|
+
if (!matchesPattern(req.method, when.method))
|
|
1400
|
+
return false;
|
|
1401
|
+
if (!matchesPattern(req.path, when.path))
|
|
1402
|
+
return false;
|
|
1403
|
+
if (!matchesPattern(req.key, when.key))
|
|
1404
|
+
return false;
|
|
1405
|
+
if (maskWhenRequiresTrace(when)) {
|
|
1406
|
+
if (!trace)
|
|
1407
|
+
return false;
|
|
1408
|
+
if (!matchesRule(when, trace))
|
|
1409
|
+
return false;
|
|
1410
|
+
}
|
|
1411
|
+
return true;
|
|
1412
|
+
}
|
|
1413
|
+
function maskKeysInPlace(node, keys, replacement) {
|
|
1414
|
+
if (!node)
|
|
1415
|
+
return;
|
|
1416
|
+
if (Array.isArray(node)) {
|
|
1417
|
+
node.forEach(item => maskKeysInPlace(item, keys, replacement));
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
if (typeof node !== 'object')
|
|
1421
|
+
return;
|
|
1422
|
+
Object.keys(node).forEach((key) => {
|
|
1423
|
+
if (matchesPattern(key, keys, false)) {
|
|
1424
|
+
try {
|
|
1425
|
+
node[key] = replacement;
|
|
1426
|
+
}
|
|
1427
|
+
catch { }
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
maskKeysInPlace(node[key], keys, replacement);
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
function maskPathInPlace(node, pathParts, replacement, depth = 0) {
|
|
1434
|
+
if (!node)
|
|
1435
|
+
return;
|
|
1436
|
+
if (depth >= pathParts.length)
|
|
1437
|
+
return;
|
|
1438
|
+
const part = pathParts[depth];
|
|
1439
|
+
const isLast = depth === pathParts.length - 1;
|
|
1440
|
+
const applyAt = (container, key) => {
|
|
1441
|
+
if (!container)
|
|
1442
|
+
return;
|
|
1443
|
+
try {
|
|
1444
|
+
container[key] = replacement;
|
|
1445
|
+
}
|
|
1446
|
+
catch { }
|
|
1447
|
+
};
|
|
1448
|
+
if (part === '*') {
|
|
1449
|
+
if (Array.isArray(node)) {
|
|
1450
|
+
for (let i = 0; i < node.length; i++) {
|
|
1451
|
+
if (isLast)
|
|
1452
|
+
applyAt(node, i);
|
|
1453
|
+
else
|
|
1454
|
+
maskPathInPlace(node[i], pathParts, replacement, depth + 1);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
else if (typeof node === 'object') {
|
|
1458
|
+
for (const key of Object.keys(node)) {
|
|
1459
|
+
if (isLast)
|
|
1460
|
+
applyAt(node, key);
|
|
1461
|
+
else
|
|
1462
|
+
maskPathInPlace(node[key], pathParts, replacement, depth + 1);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
const index = Number(part);
|
|
1468
|
+
const isIndex = Number.isInteger(index) && String(index) === part;
|
|
1469
|
+
if (Array.isArray(node) && isIndex) {
|
|
1470
|
+
if (index < 0 || index >= node.length)
|
|
1471
|
+
return;
|
|
1472
|
+
if (isLast)
|
|
1473
|
+
applyAt(node, index);
|
|
1474
|
+
else
|
|
1475
|
+
maskPathInPlace(node[index], pathParts, replacement, depth + 1);
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
if (typeof node !== 'object')
|
|
1479
|
+
return;
|
|
1480
|
+
if (!Object.prototype.hasOwnProperty.call(node, part))
|
|
1481
|
+
return;
|
|
1482
|
+
if (isLast)
|
|
1483
|
+
applyAt(node, part);
|
|
1484
|
+
else
|
|
1485
|
+
maskPathInPlace(node[part], pathParts, replacement, depth + 1);
|
|
1486
|
+
}
|
|
1487
|
+
function applyMasking(target, value, req, trace, masking) {
|
|
1488
|
+
if (!masking || !masking.rules.length)
|
|
1489
|
+
return value;
|
|
1490
|
+
if (value === undefined)
|
|
1491
|
+
return value;
|
|
1492
|
+
for (const rule of masking.rules) {
|
|
1493
|
+
if (!rule.targets.includes(target))
|
|
1494
|
+
continue;
|
|
1495
|
+
if (!matchesMaskWhen(rule.when, req, trace))
|
|
1496
|
+
continue;
|
|
1497
|
+
const replacement = rule.replacement ?? masking.replacement ?? DEFAULT_MASK_REPLACEMENT;
|
|
1498
|
+
if (rule.keys)
|
|
1499
|
+
maskKeysInPlace(value, rule.keys, replacement);
|
|
1500
|
+
if (rule.paths.length) {
|
|
1501
|
+
rule.paths.forEach(parts => maskPathInPlace(value, parts, replacement, 0));
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return value;
|
|
1505
|
+
}
|
|
1506
|
+
function reproMiddleware(cfg) {
|
|
1507
|
+
const masking = normalizeMaskingConfig(cfg.masking);
|
|
1508
|
+
return function (req, res, next) {
|
|
1509
|
+
const sid = req.headers['x-bug-session-id'] || '';
|
|
1510
|
+
const aid = req.headers['x-bug-action-id'] || '';
|
|
1511
|
+
if (!sid || !aid)
|
|
1512
|
+
return next(); // only capture tagged requests
|
|
1513
|
+
const requestStartRaw = Date.now();
|
|
1514
|
+
const headerTs = readHeaderNumber(req.headers[REQUEST_START_HEADER]);
|
|
1515
|
+
const clockSkewMs = headerTs !== null ? headerTs - requestStartRaw : 0;
|
|
1516
|
+
const requestEpochMs = headerTs !== null ? headerTs : requestStartRaw + clockSkewMs;
|
|
1517
|
+
const rid = nextRequestId(requestEpochMs);
|
|
1518
|
+
const t0 = requestStartRaw;
|
|
1519
|
+
const url = req.originalUrl || req.url || '/';
|
|
1520
|
+
const urlPathOnly = (url || '/').split('?')[0] || '/';
|
|
1521
|
+
const path = url; // back-compat
|
|
1522
|
+
const key = normalizeRouteKey(req.method, url);
|
|
1523
|
+
const maskReq = { method: String(req.method || 'GET').toUpperCase(), path: urlPathOnly, key };
|
|
1524
|
+
const requestHeaders = sanitizeHeaders(req.headers, cfg.captureHeaders);
|
|
1525
|
+
beginSessionRequest(sid);
|
|
1526
|
+
// ---- response body capture (unchanged) ----
|
|
1527
|
+
let capturedBody = undefined;
|
|
1528
|
+
const origJson = res.json.bind(res);
|
|
1529
|
+
res.json = (body) => { capturedBody = body; return origJson(body); };
|
|
1530
|
+
const origSend = res.send.bind(res);
|
|
1531
|
+
res.send = (body) => {
|
|
1532
|
+
if (capturedBody === undefined) {
|
|
1533
|
+
capturedBody = coerceBodyToStorable(body, res.getHeader?.('content-type'));
|
|
1534
|
+
}
|
|
1535
|
+
return origSend(body);
|
|
1536
|
+
};
|
|
1537
|
+
const origWrite = res.write.bind(res);
|
|
1538
|
+
const origEnd = res.end.bind(res);
|
|
1539
|
+
const chunks = [];
|
|
1540
|
+
res.write = (chunk, ...args) => { try {
|
|
1541
|
+
if (chunk != null)
|
|
1542
|
+
chunks.push(chunk);
|
|
1543
|
+
}
|
|
1544
|
+
catch { } return origWrite(chunk, ...args); };
|
|
1545
|
+
res.end = (chunk, ...args) => { try {
|
|
1546
|
+
if (chunk != null)
|
|
1547
|
+
chunks.push(chunk);
|
|
1548
|
+
}
|
|
1549
|
+
catch { } return origEnd(chunk, ...args); };
|
|
1550
|
+
// ---- our ALS (unchanged) ----
|
|
1551
|
+
const tracerApiWithTrace = __TRACER__;
|
|
1552
|
+
const runInTrace = (fn) => {
|
|
1553
|
+
if (tracerApiWithTrace?.withTrace) {
|
|
1554
|
+
return tracerApiWithTrace.withTrace(rid, fn);
|
|
1555
|
+
}
|
|
1556
|
+
return fn();
|
|
1557
|
+
};
|
|
1558
|
+
runInTrace(() => als.run({ sid, aid, clockSkewMs, excludedSpanIds: new Set() }, () => {
|
|
1559
|
+
const events = [];
|
|
1560
|
+
let endpointTrace = null;
|
|
1561
|
+
let preferredAppTrace = null;
|
|
1562
|
+
let firstAppTrace = null;
|
|
1563
|
+
let unsubscribe;
|
|
1564
|
+
let flushed = false;
|
|
1565
|
+
let finished = false;
|
|
1566
|
+
let finishedAt = null;
|
|
1567
|
+
let lastEventAt = Date.now();
|
|
1568
|
+
let idleTimer = null;
|
|
1569
|
+
let hardStopTimer = null;
|
|
1570
|
+
let drainTimer = null;
|
|
1571
|
+
let flushPayload = null;
|
|
1572
|
+
const activeSpans = new Set();
|
|
1573
|
+
let anonymousSpanDepth = 0;
|
|
1574
|
+
const ACTIVE_SPAN_FORCE_FLUSH_MS = 60000; // hard cutoff after finish to avoid endlessly running replies
|
|
1575
|
+
const clearTimers = () => {
|
|
1576
|
+
if (idleTimer) {
|
|
1577
|
+
try {
|
|
1578
|
+
clearTimeout(idleTimer);
|
|
1579
|
+
}
|
|
1580
|
+
catch { }
|
|
1581
|
+
idleTimer = null;
|
|
1582
|
+
}
|
|
1583
|
+
if (hardStopTimer) {
|
|
1584
|
+
try {
|
|
1585
|
+
clearTimeout(hardStopTimer);
|
|
1586
|
+
}
|
|
1587
|
+
catch { }
|
|
1588
|
+
hardStopTimer = null;
|
|
1589
|
+
}
|
|
1590
|
+
if (drainTimer) {
|
|
1591
|
+
try {
|
|
1592
|
+
clearTimeout(drainTimer);
|
|
1593
|
+
}
|
|
1594
|
+
catch { }
|
|
1595
|
+
drainTimer = null;
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
const hasActiveWork = () => activeSpans.size > 0 || anonymousSpanDepth > 0;
|
|
1599
|
+
const scheduleIdleFlush = (delay = TRACE_IDLE_FLUSH_MS) => {
|
|
1600
|
+
if (!finished || flushed)
|
|
1601
|
+
return;
|
|
1602
|
+
if (hasActiveWork())
|
|
1603
|
+
return;
|
|
1604
|
+
if (idleTimer) {
|
|
1605
|
+
try {
|
|
1606
|
+
clearTimeout(idleTimer);
|
|
1607
|
+
}
|
|
1608
|
+
catch { }
|
|
1609
|
+
}
|
|
1610
|
+
idleTimer = setTimeout(() => doFlush(false), delay);
|
|
1611
|
+
};
|
|
1612
|
+
const doFlush = (force = false) => {
|
|
1613
|
+
if (flushed)
|
|
1614
|
+
return;
|
|
1615
|
+
const now = Date.now();
|
|
1616
|
+
const stillActive = hasActiveWork();
|
|
1617
|
+
const quietMs = now - lastEventAt;
|
|
1618
|
+
const waitedFinish = finishedAt === null ? 0 : now - finishedAt;
|
|
1619
|
+
// If work is still active and we haven't been quiet long enough, defer.
|
|
1620
|
+
if (stillActive && !force) {
|
|
1621
|
+
const remaining = Math.max(0, TRACE_LINGER_AFTER_FINISH_MS - quietMs);
|
|
1622
|
+
scheduleIdleFlush(Math.max(remaining, 10));
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
if (stillActive && force) {
|
|
1626
|
+
// Allow forced flush after either linger window of silence or max guard.
|
|
1627
|
+
if (quietMs < TRACE_LINGER_AFTER_FINISH_MS && waitedFinish < ACTIVE_SPAN_FORCE_FLUSH_MS) {
|
|
1628
|
+
const remainingQuiet = TRACE_LINGER_AFTER_FINISH_MS - quietMs;
|
|
1629
|
+
const remainingGuard = ACTIVE_SPAN_FORCE_FLUSH_MS - waitedFinish;
|
|
1630
|
+
if (hardStopTimer) {
|
|
1631
|
+
try {
|
|
1632
|
+
clearTimeout(hardStopTimer);
|
|
1633
|
+
}
|
|
1634
|
+
catch { }
|
|
1635
|
+
}
|
|
1636
|
+
hardStopTimer = setTimeout(() => doFlush(true), Math.max(10, Math.min(remainingQuiet, remainingGuard)));
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
flushed = true;
|
|
1641
|
+
clearTimers();
|
|
1642
|
+
try {
|
|
1643
|
+
unsubscribe && unsubscribe();
|
|
1644
|
+
}
|
|
1645
|
+
catch { }
|
|
1646
|
+
try {
|
|
1647
|
+
flushPayload?.();
|
|
1648
|
+
}
|
|
1649
|
+
catch { }
|
|
1650
|
+
};
|
|
1651
|
+
const bumpIdle = () => {
|
|
1652
|
+
if (!finished || flushed)
|
|
1653
|
+
return;
|
|
1654
|
+
if (idleTimer) {
|
|
1655
|
+
try {
|
|
1656
|
+
clearTimeout(idleTimer);
|
|
1657
|
+
}
|
|
1658
|
+
catch { }
|
|
1659
|
+
}
|
|
1660
|
+
scheduleIdleFlush();
|
|
1661
|
+
};
|
|
1662
|
+
const normalizeSpanId = (val) => {
|
|
1663
|
+
if (val === null || val === undefined)
|
|
1664
|
+
return null;
|
|
1665
|
+
const str = String(val);
|
|
1666
|
+
return str ? str : null;
|
|
1667
|
+
};
|
|
1668
|
+
try {
|
|
1669
|
+
if (__TRACER__?.tracer?.on) {
|
|
1670
|
+
const getTid = __TRACER__?.getCurrentTraceId;
|
|
1671
|
+
const tidNow = getTid ? getTid() : null;
|
|
1672
|
+
if (tidNow) {
|
|
1673
|
+
unsubscribe = __TRACER__.tracer.on((ev) => {
|
|
1674
|
+
if (!ev || ev.traceId !== tidNow)
|
|
1675
|
+
return;
|
|
1676
|
+
const sourceFileForLibrary = typeof ev.sourceFile === 'string' && ev.sourceFile
|
|
1677
|
+
? String(ev.sourceFile)
|
|
1678
|
+
: null;
|
|
1679
|
+
const evt = {
|
|
1680
|
+
t: alignTimestamp(ev.t),
|
|
1681
|
+
type: ev.type,
|
|
1682
|
+
fn: ev.fn,
|
|
1683
|
+
file: ev.file,
|
|
1684
|
+
line: ev.line,
|
|
1685
|
+
depth: ev.depth,
|
|
1686
|
+
spanId: ev.spanId ?? null,
|
|
1687
|
+
parentSpanId: ev.parentSpanId ?? null,
|
|
1688
|
+
};
|
|
1689
|
+
if (ev.functionType !== undefined) {
|
|
1690
|
+
evt.functionType = ev.functionType;
|
|
1691
|
+
}
|
|
1692
|
+
const candidate = {
|
|
1693
|
+
type: evt.type,
|
|
1694
|
+
eventType: evt.type,
|
|
1695
|
+
functionType: evt.functionType ?? null,
|
|
1696
|
+
fn: evt.fn,
|
|
1697
|
+
wrapperClass: inferWrapperClassFromFn(evt.fn),
|
|
1698
|
+
file: evt.file ?? null,
|
|
1699
|
+
line: evt.line ?? null,
|
|
1700
|
+
depth: evt.depth,
|
|
1701
|
+
library: inferLibraryNameFromFile(sourceFileForLibrary ?? evt.file),
|
|
1702
|
+
};
|
|
1703
|
+
if (ev.args !== undefined) {
|
|
1704
|
+
evt.args = applyMasking('trace.args', sanitizeTraceArgs(ev.args), maskReq, candidate, masking);
|
|
1705
|
+
}
|
|
1706
|
+
if (ev.returnValue !== undefined) {
|
|
1707
|
+
evt.returnValue = applyMasking('trace.returnValue', sanitizeTraceValue(ev.returnValue), maskReq, candidate, masking);
|
|
1708
|
+
}
|
|
1709
|
+
if (ev.error !== undefined) {
|
|
1710
|
+
evt.error = applyMasking('trace.error', sanitizeTraceValue(ev.error), maskReq, candidate, masking);
|
|
1711
|
+
}
|
|
1712
|
+
if (ev.threw !== undefined) {
|
|
1713
|
+
evt.threw = Boolean(ev.threw);
|
|
1714
|
+
}
|
|
1715
|
+
if (ev.unawaited !== undefined) {
|
|
1716
|
+
evt.unawaited = ev.unawaited === true;
|
|
1717
|
+
}
|
|
1718
|
+
const dropEvent = shouldDropTraceEvent(candidate);
|
|
1719
|
+
const dropSpanTree = shouldPropagateUserDisabledFunctionTrace(candidate);
|
|
1720
|
+
const spanKey = normalizeSpanId(evt.spanId);
|
|
1721
|
+
const parentSpanKey = normalizeSpanId(evt.parentSpanId);
|
|
1722
|
+
const isChildOfExcluded = parentSpanKey ? isExcludedSpanId(parentSpanKey) : false;
|
|
1723
|
+
const isExcluded = spanKey ? isExcludedSpanId(spanKey) : false;
|
|
1724
|
+
if (evt.type === 'enter') {
|
|
1725
|
+
lastEventAt = Date.now();
|
|
1726
|
+
if (spanKey) {
|
|
1727
|
+
activeSpans.add(spanKey);
|
|
1728
|
+
}
|
|
1729
|
+
else {
|
|
1730
|
+
anonymousSpanDepth = Math.max(0, anonymousSpanDepth + 1);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
else if (evt.type === 'exit') {
|
|
1734
|
+
lastEventAt = Date.now();
|
|
1735
|
+
if (spanKey && activeSpans.has(spanKey)) {
|
|
1736
|
+
activeSpans.delete(spanKey);
|
|
1737
|
+
}
|
|
1738
|
+
else if (!spanKey && anonymousSpanDepth > 0) {
|
|
1739
|
+
anonymousSpanDepth = Math.max(0, anonymousSpanDepth - 1);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
if (dropEvent || isChildOfExcluded || isExcluded) {
|
|
1743
|
+
if (evt.type === 'enter' && spanKey && (dropSpanTree || isChildOfExcluded || isExcluded)) {
|
|
1744
|
+
try {
|
|
1745
|
+
getCtx().excludedSpanIds?.add(spanKey);
|
|
1746
|
+
}
|
|
1747
|
+
catch { }
|
|
1748
|
+
}
|
|
1749
|
+
if (finished) {
|
|
1750
|
+
scheduleIdleFlush();
|
|
1751
|
+
}
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
if (finished) {
|
|
1755
|
+
scheduleIdleFlush();
|
|
1756
|
+
}
|
|
1757
|
+
if (evt.type === 'enter' && isLikelyAppFile(evt.file)) {
|
|
1758
|
+
const depthOk = evt.depth === undefined || evt.depth <= 6;
|
|
1759
|
+
const trace = toEndpointTrace(evt);
|
|
1760
|
+
if (depthOk && !firstAppTrace) {
|
|
1761
|
+
firstAppTrace = trace;
|
|
1762
|
+
}
|
|
1763
|
+
if (isLikelyNestControllerFile(evt.file)) {
|
|
1764
|
+
endpointTrace = trace;
|
|
1765
|
+
}
|
|
1766
|
+
else if (depthOk && !preferredAppTrace && !isLikelyNestGuardFile(evt.file)) {
|
|
1767
|
+
preferredAppTrace = trace;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
events.push(evt);
|
|
1771
|
+
bumpIdle();
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
catch { /* never break user code */ }
|
|
1777
|
+
const handleDone = () => {
|
|
1778
|
+
if (finished && flushed)
|
|
1779
|
+
return;
|
|
1780
|
+
finished = true;
|
|
1781
|
+
if (finishedAt === null) {
|
|
1782
|
+
finishedAt = Date.now();
|
|
1783
|
+
}
|
|
1784
|
+
if (capturedBody === undefined && chunks.length) {
|
|
1785
|
+
const buf = Buffer.isBuffer(chunks[0])
|
|
1786
|
+
? Buffer.concat(chunks.map(c => (Buffer.isBuffer(c) ? c : Buffer.from(String(c)))))
|
|
1787
|
+
: Buffer.from(chunks.map(String).join(''));
|
|
1788
|
+
capturedBody = coerceBodyToStorable(buf, res.getHeader?.('content-type'));
|
|
1789
|
+
}
|
|
1790
|
+
if (!flushPayload) {
|
|
1791
|
+
flushPayload = () => {
|
|
1792
|
+
const baseEvents = balanceTraceEvents(events.slice());
|
|
1793
|
+
const orderedEvents = TRACE_ORDER_MODE === 'tree'
|
|
1794
|
+
? reorderTraceEvents(baseEvents)
|
|
1795
|
+
: sortTraceEventsChronologically(baseEvents);
|
|
1796
|
+
const summary = summarizeEndpointFromEvents(orderedEvents);
|
|
1797
|
+
const chosenEndpoint = summary.endpointTrace
|
|
1798
|
+
?? summary.preferredAppTrace
|
|
1799
|
+
?? summary.firstAppTrace
|
|
1800
|
+
?? endpointTrace
|
|
1801
|
+
?? preferredAppTrace
|
|
1802
|
+
?? firstAppTrace
|
|
1803
|
+
?? { fn: null, file: null, line: null, functionType: null };
|
|
1804
|
+
const traceBatches = chunkArray(orderedEvents, TRACE_BATCH_SIZE);
|
|
1805
|
+
const endpointTraceCtx = (() => {
|
|
1806
|
+
if (!chosenEndpoint?.fn && !chosenEndpoint?.file)
|
|
1807
|
+
return null;
|
|
1808
|
+
return {
|
|
1809
|
+
type: 'enter',
|
|
1810
|
+
eventType: 'enter',
|
|
1811
|
+
fn: chosenEndpoint.fn ?? undefined,
|
|
1812
|
+
wrapperClass: inferWrapperClassFromFn(chosenEndpoint.fn),
|
|
1813
|
+
file: chosenEndpoint.file ?? null,
|
|
1814
|
+
line: chosenEndpoint.line ?? null,
|
|
1815
|
+
functionType: chosenEndpoint.functionType ?? null,
|
|
1816
|
+
library: inferLibraryNameFromFile(chosenEndpoint.file),
|
|
1817
|
+
};
|
|
1818
|
+
})();
|
|
1819
|
+
const requestBody = applyMasking('request.body', sanitizeRequestSnapshot(req.body), maskReq, endpointTraceCtx, masking);
|
|
1820
|
+
const requestParams = applyMasking('request.params', sanitizeRequestSnapshot(req.params), maskReq, endpointTraceCtx, masking);
|
|
1821
|
+
const requestQuery = applyMasking('request.query', sanitizeRequestSnapshot(req.query), maskReq, endpointTraceCtx, masking);
|
|
1822
|
+
const maskedHeaders = applyMasking('request.headers', requestHeaders, maskReq, endpointTraceCtx, masking);
|
|
1823
|
+
const responseBody = applyMasking('response.body', capturedBody === undefined ? undefined : sanitizeRequestSnapshot(capturedBody), maskReq, endpointTraceCtx, masking);
|
|
1824
|
+
const requestPayload = {
|
|
1825
|
+
rid,
|
|
1826
|
+
method: req.method,
|
|
1827
|
+
url,
|
|
1828
|
+
path,
|
|
1829
|
+
status: res.statusCode,
|
|
1830
|
+
durMs: Date.now() - t0,
|
|
1831
|
+
headers: maskedHeaders,
|
|
1832
|
+
key,
|
|
1833
|
+
respBody: responseBody,
|
|
1834
|
+
trace: traceBatches.length ? undefined : '[]',
|
|
1835
|
+
};
|
|
1836
|
+
if (requestBody !== undefined)
|
|
1837
|
+
requestPayload.body = requestBody;
|
|
1838
|
+
if (requestParams !== undefined)
|
|
1839
|
+
requestPayload.params = requestParams;
|
|
1840
|
+
if (requestQuery !== undefined)
|
|
1841
|
+
requestPayload.query = requestQuery;
|
|
1842
|
+
requestPayload.entryPoint = chosenEndpoint;
|
|
1843
|
+
post(cfg, sid, {
|
|
1844
|
+
entries: [{
|
|
1845
|
+
actionId: aid,
|
|
1846
|
+
request: requestPayload,
|
|
1847
|
+
t: alignedNow(),
|
|
1848
|
+
}]
|
|
1849
|
+
});
|
|
1850
|
+
if (traceBatches.length) {
|
|
1851
|
+
for (let i = 0; i < traceBatches.length; i++) {
|
|
1852
|
+
const batch = traceBatches[i];
|
|
1853
|
+
let traceStr = '[]';
|
|
1854
|
+
try {
|
|
1855
|
+
traceStr = JSON.stringify(batch);
|
|
1856
|
+
}
|
|
1857
|
+
catch { }
|
|
1858
|
+
post(cfg, sid, {
|
|
1859
|
+
entries: [{
|
|
1860
|
+
actionId: aid,
|
|
1861
|
+
trace: traceStr,
|
|
1862
|
+
traceBatch: {
|
|
1863
|
+
rid,
|
|
1864
|
+
index: i,
|
|
1865
|
+
total: traceBatches.length,
|
|
1866
|
+
},
|
|
1867
|
+
t: alignedNow(),
|
|
1868
|
+
}],
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
const waitDrain = waitForSessionDrain(sid);
|
|
1875
|
+
endSessionRequest(sid);
|
|
1876
|
+
if (SESSION_DRAIN_TIMEOUT_MS > 0) {
|
|
1877
|
+
drainTimer = setTimeout(() => doFlush(true), SESSION_DRAIN_TIMEOUT_MS);
|
|
1878
|
+
}
|
|
1879
|
+
waitDrain.then(() => {
|
|
1880
|
+
if (drainTimer) {
|
|
1881
|
+
try {
|
|
1882
|
+
clearTimeout(drainTimer);
|
|
1883
|
+
}
|
|
1884
|
+
catch { }
|
|
1885
|
+
drainTimer = null;
|
|
1886
|
+
}
|
|
1887
|
+
if (__TRACER_READY) {
|
|
1888
|
+
bumpIdle();
|
|
1889
|
+
const hardDeadlineMs = Math.max(TRACE_FLUSH_DELAY_MS + TRACE_LINGER_AFTER_FINISH_MS, TRACE_IDLE_FLUSH_MS + TRACE_FLUSH_DELAY_MS);
|
|
1890
|
+
hardStopTimer = setTimeout(() => doFlush(true), hardDeadlineMs);
|
|
1891
|
+
}
|
|
1892
|
+
else {
|
|
1893
|
+
doFlush(true);
|
|
1894
|
+
}
|
|
1895
|
+
}).catch(() => {
|
|
1896
|
+
doFlush(true);
|
|
1897
|
+
});
|
|
1898
|
+
};
|
|
1899
|
+
res.on('finish', handleDone);
|
|
1900
|
+
res.on('close', handleDone);
|
|
1901
|
+
next();
|
|
1902
|
+
}));
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
exports.reproMiddleware = reproMiddleware;
|
|
1906
|
+
// ===================================================================
|
|
1907
|
+
// reproMongoosePlugin — stable + NON-intrusive query logs
|
|
1908
|
+
// - NO prototype monkey-patching of Mongoose
|
|
1909
|
+
// - ONLY schema middleware (pre/post) for specific ops
|
|
1910
|
+
// - keeps your existing doc-diff hooks
|
|
1911
|
+
// ===================================================================
|
|
1912
|
+
function reproMongoosePlugin(cfg) {
|
|
1913
|
+
return function (schema) {
|
|
1914
|
+
// -------- pre/post save (unchanged) --------
|
|
1915
|
+
schema.pre('save', { document: true }, async function (next) {
|
|
1916
|
+
const { sid, aid } = getCtx();
|
|
1917
|
+
if (!sid || !aid)
|
|
1918
|
+
return next();
|
|
1919
|
+
if (this.$isSubdocument)
|
|
1920
|
+
return next();
|
|
1921
|
+
let before = null;
|
|
1922
|
+
try {
|
|
1923
|
+
if (!this.isNew) {
|
|
1924
|
+
const model = this.constructor;
|
|
1925
|
+
before = await model.findById(this._id).lean().exec();
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
catch { }
|
|
1929
|
+
this.__repro_meta = {
|
|
1930
|
+
wasNew: this.isNew,
|
|
1931
|
+
before,
|
|
1932
|
+
collection: resolveCollectionOrWarn(this, 'doc'),
|
|
1933
|
+
spanContext: captureSpanContextFromTracer(this),
|
|
1934
|
+
};
|
|
1935
|
+
next();
|
|
1936
|
+
});
|
|
1937
|
+
schema.post('save', { document: true }, function () {
|
|
1938
|
+
const { sid, aid } = getCtx();
|
|
1939
|
+
if (!sid || !aid)
|
|
1940
|
+
return;
|
|
1941
|
+
if (this.$isSubdocument)
|
|
1942
|
+
return;
|
|
1943
|
+
const meta = this.__repro_meta || {};
|
|
1944
|
+
const before = meta.before ?? null;
|
|
1945
|
+
const after = this.toObject({ depopulate: true });
|
|
1946
|
+
const collection = meta.collection || resolveCollectionOrWarn(this, 'doc');
|
|
1947
|
+
const spanContext = meta.spanContext || captureSpanContextFromTracer(this);
|
|
1948
|
+
if (!shouldCaptureDbSpan(spanContext))
|
|
1949
|
+
return;
|
|
1950
|
+
const query = meta.wasNew
|
|
1951
|
+
? { op: 'insertOne', doc: after }
|
|
1952
|
+
: { filter: { _id: this._id }, update: buildMinimalUpdate(before, after), options: { upsert: false } };
|
|
1953
|
+
post(cfg, getCtx().sid, {
|
|
1954
|
+
entries: [{
|
|
1955
|
+
actionId: getCtx().aid,
|
|
1956
|
+
db: [attachSpanContext({
|
|
1957
|
+
collection,
|
|
1958
|
+
pk: { _id: this._id },
|
|
1959
|
+
before,
|
|
1960
|
+
after,
|
|
1961
|
+
op: meta.wasNew ? 'insert' : 'update',
|
|
1962
|
+
query,
|
|
1963
|
+
}, spanContext)],
|
|
1964
|
+
t: alignedNow(),
|
|
1965
|
+
}]
|
|
1966
|
+
});
|
|
1967
|
+
});
|
|
1968
|
+
// -------- findOneAndUpdate capture (unchanged) --------
|
|
1969
|
+
schema.pre('findOneAndUpdate', async function (next) {
|
|
1970
|
+
const { sid, aid } = getCtx();
|
|
1971
|
+
if (!sid || !aid)
|
|
1972
|
+
return next();
|
|
1973
|
+
try {
|
|
1974
|
+
const filter = this.getFilter();
|
|
1975
|
+
const model = this.model;
|
|
1976
|
+
this.__repro_before = await model.findOne(filter).lean().exec();
|
|
1977
|
+
this.setOptions({ new: true });
|
|
1978
|
+
this.__repro_collection = resolveCollectionOrWarn(this, 'query');
|
|
1979
|
+
this.__repro_spanContext = captureSpanContextFromTracer(this);
|
|
1980
|
+
}
|
|
1981
|
+
catch { }
|
|
1982
|
+
next();
|
|
1983
|
+
});
|
|
1984
|
+
schema.post('findOneAndUpdate', function (res) {
|
|
1985
|
+
const { sid, aid } = getCtx();
|
|
1986
|
+
if (!sid || !aid)
|
|
1987
|
+
return;
|
|
1988
|
+
const before = this.__repro_before ?? null;
|
|
1989
|
+
const after = res ?? null;
|
|
1990
|
+
const collection = this.__repro_collection || resolveCollectionOrWarn(this, 'query');
|
|
1991
|
+
const spanContext = this.__repro_spanContext || captureSpanContextFromTracer(this);
|
|
1992
|
+
if (!shouldCaptureDbSpan(spanContext))
|
|
1993
|
+
return;
|
|
1994
|
+
const pk = after?._id ?? before?._id;
|
|
1995
|
+
post(cfg, getCtx().sid, {
|
|
1996
|
+
entries: [{
|
|
1997
|
+
actionId: getCtx().aid,
|
|
1998
|
+
db: [attachSpanContext({
|
|
1999
|
+
collection,
|
|
2000
|
+
pk: { _id: pk },
|
|
2001
|
+
before,
|
|
2002
|
+
after,
|
|
2003
|
+
op: after && before ? 'update' : after ? 'insert' : 'update',
|
|
2004
|
+
}, spanContext)],
|
|
2005
|
+
t: alignedNow()
|
|
2006
|
+
}]
|
|
2007
|
+
});
|
|
2008
|
+
});
|
|
2009
|
+
// -------- deleteOne capture (unchanged) --------
|
|
2010
|
+
schema.pre('deleteOne', { document: false, query: true }, async function (next) {
|
|
2011
|
+
const { sid, aid } = getCtx();
|
|
2012
|
+
if (!sid || !aid)
|
|
2013
|
+
return next();
|
|
2014
|
+
try {
|
|
2015
|
+
const filter = this.getFilter();
|
|
2016
|
+
this.__repro_before = await this.model.findOne(filter).lean().exec();
|
|
2017
|
+
this.__repro_collection = resolveCollectionOrWarn(this, 'query');
|
|
2018
|
+
this.__repro_filter = filter;
|
|
2019
|
+
this.__repro_spanContext = captureSpanContextFromTracer(this);
|
|
2020
|
+
}
|
|
2021
|
+
catch { }
|
|
2022
|
+
next();
|
|
2023
|
+
});
|
|
2024
|
+
schema.post('deleteOne', { document: false, query: true }, function () {
|
|
2025
|
+
const { sid, aid } = getCtx();
|
|
2026
|
+
if (!sid || !aid)
|
|
2027
|
+
return;
|
|
2028
|
+
const before = this.__repro_before ?? null;
|
|
2029
|
+
if (!before)
|
|
2030
|
+
return;
|
|
2031
|
+
const collection = this.__repro_collection || resolveCollectionOrWarn(this, 'query');
|
|
2032
|
+
const filter = this.__repro_filter ?? { _id: before._id };
|
|
2033
|
+
const spanContext = this.__repro_spanContext || captureSpanContextFromTracer(this);
|
|
2034
|
+
if (!shouldCaptureDbSpan(spanContext))
|
|
2035
|
+
return;
|
|
2036
|
+
post(cfg, getCtx().sid, {
|
|
2037
|
+
entries: [{
|
|
2038
|
+
actionId: getCtx().aid,
|
|
2039
|
+
db: [attachSpanContext({
|
|
2040
|
+
collection,
|
|
2041
|
+
pk: { _id: before._id },
|
|
2042
|
+
before,
|
|
2043
|
+
after: null,
|
|
2044
|
+
op: 'delete',
|
|
2045
|
+
query: { filter },
|
|
2046
|
+
}, spanContext)],
|
|
2047
|
+
t: alignedNow()
|
|
2048
|
+
}]
|
|
2049
|
+
});
|
|
2050
|
+
});
|
|
2051
|
+
// -------- NON-intrusive generic query telemetry via schema hooks -------
|
|
2052
|
+
const sanitizeDbValue = (value) => {
|
|
2053
|
+
const sanitized = sanitizeTraceValue(value);
|
|
2054
|
+
return sanitized === undefined ? undefined : sanitized;
|
|
2055
|
+
};
|
|
2056
|
+
const READ_OPS = [
|
|
2057
|
+
'find',
|
|
2058
|
+
'findOne',
|
|
2059
|
+
'countDocuments',
|
|
2060
|
+
'estimatedDocumentCount',
|
|
2061
|
+
'distinct',
|
|
2062
|
+
];
|
|
2063
|
+
const WRITE_OPS = [
|
|
2064
|
+
'updateOne',
|
|
2065
|
+
'updateMany',
|
|
2066
|
+
'replaceOne',
|
|
2067
|
+
'deleteMany',
|
|
2068
|
+
'findOneAndUpdate',
|
|
2069
|
+
'findOneAndDelete',
|
|
2070
|
+
'findOneAndRemove',
|
|
2071
|
+
'findOneAndReplace',
|
|
2072
|
+
'findByIdAndUpdate',
|
|
2073
|
+
'findByIdAndDelete',
|
|
2074
|
+
'findByIdAndRemove',
|
|
2075
|
+
'findByIdAndReplace',
|
|
2076
|
+
];
|
|
2077
|
+
function attachQueryHooks(op) {
|
|
2078
|
+
schema.pre(op, function (next) {
|
|
2079
|
+
try {
|
|
2080
|
+
this.__repro_qmeta = {
|
|
2081
|
+
t0: Date.now(),
|
|
2082
|
+
collection: this?.model?.collection?.name || 'unknown',
|
|
2083
|
+
op,
|
|
2084
|
+
spanContext: captureSpanContextFromTracer(this),
|
|
2085
|
+
filter: sanitizeDbValue(this.getFilter?.() ?? this._conditions ?? undefined),
|
|
2086
|
+
update: sanitizeDbValue(this.getUpdate?.() ?? this._update ?? undefined),
|
|
2087
|
+
projection: sanitizeDbValue(this.projection?.() ?? this._fields ?? undefined),
|
|
2088
|
+
options: sanitizeDbValue(this.getOptions?.() ?? this.options ?? undefined),
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
catch {
|
|
2092
|
+
this.__repro_qmeta = {
|
|
2093
|
+
t0: Date.now(),
|
|
2094
|
+
collection: 'unknown',
|
|
2095
|
+
op,
|
|
2096
|
+
spanContext: captureSpanContextFromTracer(this),
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
next();
|
|
2100
|
+
});
|
|
2101
|
+
schema.post(op, function (res) {
|
|
2102
|
+
const { sid, aid } = getCtx();
|
|
2103
|
+
if (!sid)
|
|
2104
|
+
return;
|
|
2105
|
+
const meta = this.__repro_qmeta || { t0: Date.now(), collection: 'unknown', op };
|
|
2106
|
+
const resultMeta = summarizeQueryResult(op, res);
|
|
2107
|
+
const spanContext = meta.spanContext || captureSpanContextFromTracer(this);
|
|
2108
|
+
emitDbQuery(cfg, sid, aid, {
|
|
2109
|
+
collection: meta.collection,
|
|
2110
|
+
op,
|
|
2111
|
+
query: { filter: meta.filter, update: meta.update, projection: meta.projection, options: meta.options },
|
|
2112
|
+
resultMeta,
|
|
2113
|
+
durMs: Date.now() - meta.t0,
|
|
2114
|
+
t: alignedNow(),
|
|
2115
|
+
spanContext,
|
|
2116
|
+
});
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
READ_OPS.forEach(attachQueryHooks);
|
|
2120
|
+
WRITE_OPS.forEach(attachQueryHooks);
|
|
2121
|
+
// bulkWrite + insertMany (non-query middleware)
|
|
2122
|
+
schema.pre('insertMany', { document: false, query: false }, function (next, docs) {
|
|
2123
|
+
try {
|
|
2124
|
+
this.__repro_insert_meta = {
|
|
2125
|
+
t0: Date.now(),
|
|
2126
|
+
collection: this?.collection?.name || this?.model?.collection?.name || 'unknown',
|
|
2127
|
+
docs: sanitizeDbValue(docs),
|
|
2128
|
+
spanContext: captureSpanContextFromTracer(this),
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
catch {
|
|
2132
|
+
this.__repro_insert_meta = {
|
|
2133
|
+
t0: Date.now(),
|
|
2134
|
+
collection: 'unknown',
|
|
2135
|
+
spanContext: captureSpanContextFromTracer(this),
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
next();
|
|
2139
|
+
});
|
|
2140
|
+
schema.post('insertMany', { document: false, query: false }, function (docs) {
|
|
2141
|
+
const { sid, aid } = getCtx();
|
|
2142
|
+
if (!sid)
|
|
2143
|
+
return;
|
|
2144
|
+
const meta = this.__repro_insert_meta || { t0: Date.now(), collection: 'unknown' };
|
|
2145
|
+
const resultMeta = Array.isArray(docs) ? { inserted: docs.length } : summarizeQueryResult('insertMany', docs);
|
|
2146
|
+
const spanContext = meta.spanContext || captureSpanContextFromTracer(this);
|
|
2147
|
+
emitDbQuery(cfg, sid, aid, {
|
|
2148
|
+
collection: meta.collection,
|
|
2149
|
+
op: 'insertMany',
|
|
2150
|
+
query: { docs: meta.docs ?? undefined },
|
|
2151
|
+
resultMeta,
|
|
2152
|
+
durMs: Date.now() - meta.t0,
|
|
2153
|
+
t: alignedNow(),
|
|
2154
|
+
spanContext,
|
|
2155
|
+
});
|
|
2156
|
+
});
|
|
2157
|
+
schema.pre('bulkWrite', { document: false, query: false }, function (next, ops) {
|
|
2158
|
+
try {
|
|
2159
|
+
this.__repro_bulk_meta = {
|
|
2160
|
+
t0: Date.now(),
|
|
2161
|
+
collection: this?.collection?.name || this?.model?.collection?.name || 'unknown',
|
|
2162
|
+
ops: sanitizeDbValue(ops),
|
|
2163
|
+
spanContext: captureSpanContextFromTracer(this),
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
catch {
|
|
2167
|
+
this.__repro_bulk_meta = {
|
|
2168
|
+
t0: Date.now(),
|
|
2169
|
+
collection: 'unknown',
|
|
2170
|
+
spanContext: captureSpanContextFromTracer(this),
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
next();
|
|
2174
|
+
});
|
|
2175
|
+
schema.post('bulkWrite', { document: false, query: false }, function (res) {
|
|
2176
|
+
const { sid, aid } = getCtx();
|
|
2177
|
+
if (!sid)
|
|
2178
|
+
return;
|
|
2179
|
+
const meta = this.__repro_bulk_meta || { t0: Date.now(), collection: 'unknown' };
|
|
2180
|
+
const bulkResult = summarizeBulkResult(res);
|
|
2181
|
+
const resultMeta = { ...bulkResult, result: sanitizeResultForMeta(res?.result ?? res) };
|
|
2182
|
+
const spanContext = meta.spanContext || captureSpanContextFromTracer(this);
|
|
2183
|
+
emitDbQuery(cfg, sid, aid, {
|
|
2184
|
+
collection: meta.collection,
|
|
2185
|
+
op: 'bulkWrite',
|
|
2186
|
+
query: { ops: meta.ops ?? undefined },
|
|
2187
|
+
resultMeta,
|
|
2188
|
+
durMs: Date.now() - meta.t0,
|
|
2189
|
+
t: alignedNow(),
|
|
2190
|
+
spanContext,
|
|
2191
|
+
});
|
|
2192
|
+
});
|
|
2193
|
+
// Aggregate middleware (non-intrusive)
|
|
2194
|
+
schema.pre('aggregate', function (next) {
|
|
2195
|
+
try {
|
|
2196
|
+
this.__repro_aggmeta = {
|
|
2197
|
+
t0: Date.now(),
|
|
2198
|
+
collection: this?.model?.collection?.name ||
|
|
2199
|
+
this?._model?.collection?.name ||
|
|
2200
|
+
(this?.model && this.model.collection?.name) ||
|
|
2201
|
+
'unknown',
|
|
2202
|
+
spanContext: captureSpanContextFromTracer(this),
|
|
2203
|
+
pipeline: sanitizeDbValue(this.pipeline?.() ?? this._pipeline ?? undefined),
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
catch {
|
|
2207
|
+
this.__repro_aggmeta = {
|
|
2208
|
+
t0: Date.now(),
|
|
2209
|
+
collection: 'unknown',
|
|
2210
|
+
pipeline: undefined,
|
|
2211
|
+
spanContext: captureSpanContextFromTracer(this),
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
next();
|
|
2215
|
+
});
|
|
2216
|
+
schema.post('aggregate', function (res) {
|
|
2217
|
+
const { sid, aid } = getCtx();
|
|
2218
|
+
if (!sid)
|
|
2219
|
+
return;
|
|
2220
|
+
const meta = this.__repro_aggmeta || { t0: Date.now(), collection: 'unknown' };
|
|
2221
|
+
const resultMeta = summarizeQueryResult('aggregate', res);
|
|
2222
|
+
const spanContext = meta.spanContext || captureSpanContextFromTracer(this);
|
|
2223
|
+
emitDbQuery(cfg, sid, aid, {
|
|
2224
|
+
collection: meta.collection,
|
|
2225
|
+
op: 'aggregate',
|
|
2226
|
+
query: { pipeline: meta.pipeline },
|
|
2227
|
+
resultMeta,
|
|
2228
|
+
durMs: Date.now() - meta.t0,
|
|
2229
|
+
t: alignedNow(),
|
|
2230
|
+
spanContext,
|
|
2231
|
+
});
|
|
2232
|
+
});
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
exports.reproMongoosePlugin = reproMongoosePlugin;
|
|
2236
|
+
function summarizeQueryResult(op, res) {
|
|
2237
|
+
const resultPreview = sanitizeResultForMeta(res);
|
|
2238
|
+
if (op === 'find' ||
|
|
2239
|
+
op === 'findOne' ||
|
|
2240
|
+
op === 'aggregate' ||
|
|
2241
|
+
op === 'distinct' ||
|
|
2242
|
+
op.startsWith('count')) {
|
|
2243
|
+
const summary = {};
|
|
2244
|
+
if (Array.isArray(res))
|
|
2245
|
+
summary.docsCount = res.length;
|
|
2246
|
+
else if (res && typeof res === 'object' && typeof res.toArray === 'function')
|
|
2247
|
+
summary.docsCount = undefined;
|
|
2248
|
+
else if (res == null)
|
|
2249
|
+
summary.docsCount = 0;
|
|
2250
|
+
else
|
|
2251
|
+
summary.docsCount = 1;
|
|
2252
|
+
if (typeof resultPreview !== 'undefined') {
|
|
2253
|
+
summary.result = resultPreview;
|
|
2254
|
+
}
|
|
2255
|
+
return summary;
|
|
2256
|
+
}
|
|
2257
|
+
if (op === 'insertMany') {
|
|
2258
|
+
const summary = {};
|
|
2259
|
+
if (Array.isArray(res))
|
|
2260
|
+
summary.inserted = res.length;
|
|
2261
|
+
if (typeof resultPreview !== 'undefined')
|
|
2262
|
+
summary.result = resultPreview;
|
|
2263
|
+
return summary;
|
|
2264
|
+
}
|
|
2265
|
+
if (op === 'bulkWrite') {
|
|
2266
|
+
return { ...summarizeBulkResult(res), result: resultPreview };
|
|
2267
|
+
}
|
|
2268
|
+
const stats = pickWriteStats(res);
|
|
2269
|
+
if (typeof resultPreview !== 'undefined') {
|
|
2270
|
+
return { ...stats, result: resultPreview };
|
|
2271
|
+
}
|
|
2272
|
+
return stats;
|
|
2273
|
+
}
|
|
2274
|
+
function summarizeBulkResult(res) {
|
|
2275
|
+
return {
|
|
2276
|
+
matched: res?.matchedCount ?? res?.nMatched ?? undefined,
|
|
2277
|
+
modified: res?.modifiedCount ?? res?.nModified ?? undefined,
|
|
2278
|
+
upserted: res?.upsertedCount ?? undefined,
|
|
2279
|
+
deleted: res?.deletedCount ?? undefined,
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
function pickWriteStats(r) {
|
|
2283
|
+
return {
|
|
2284
|
+
matched: r?.matchedCount ?? r?.n ?? r?.nMatched ?? undefined,
|
|
2285
|
+
modified: r?.modifiedCount ?? r?.nModified ?? undefined,
|
|
2286
|
+
upsertedId: r?.upsertedId ?? r?.upserted?._id ?? undefined,
|
|
2287
|
+
deleted: r?.deletedCount ?? undefined,
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
2290
|
+
function safeJson(v) {
|
|
2291
|
+
try {
|
|
2292
|
+
return v == null ? undefined : JSON.parse(JSON.stringify(v));
|
|
2293
|
+
}
|
|
2294
|
+
catch {
|
|
2295
|
+
return undefined;
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
function sanitizeResultForMeta(value) {
|
|
2299
|
+
if (value === undefined)
|
|
2300
|
+
return undefined;
|
|
2301
|
+
if (typeof value === 'function')
|
|
2302
|
+
return undefined;
|
|
2303
|
+
try {
|
|
2304
|
+
return sanitizeTraceValue(value);
|
|
2305
|
+
}
|
|
2306
|
+
catch {
|
|
2307
|
+
const fallback = safeJson(value);
|
|
2308
|
+
return fallback === undefined ? undefined : fallback;
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
function dehydrateComplexValue(value) {
|
|
2312
|
+
if (!value || typeof value !== 'object')
|
|
2313
|
+
return value;
|
|
2314
|
+
if (Array.isArray(value))
|
|
2315
|
+
return value;
|
|
2316
|
+
if (value instanceof Date || value instanceof RegExp || Buffer.isBuffer(value))
|
|
2317
|
+
return value;
|
|
2318
|
+
if (value instanceof Map || value instanceof Set)
|
|
2319
|
+
return value;
|
|
2320
|
+
try {
|
|
2321
|
+
if (typeof value.toJSON === 'function') {
|
|
2322
|
+
const plain = value.toJSON();
|
|
2323
|
+
if (plain && plain !== value)
|
|
2324
|
+
return plain;
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
catch { }
|
|
2328
|
+
try {
|
|
2329
|
+
if (typeof value.toObject === 'function') {
|
|
2330
|
+
const plain = value.toObject();
|
|
2331
|
+
if (plain && plain !== value)
|
|
2332
|
+
return plain;
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
catch { }
|
|
2336
|
+
const ctor = value?.constructor?.name;
|
|
2337
|
+
if (ctor && ctor !== 'Object') {
|
|
2338
|
+
const plain = safeJson(value);
|
|
2339
|
+
if (plain !== undefined)
|
|
2340
|
+
return plain;
|
|
2341
|
+
}
|
|
2342
|
+
return value;
|
|
2343
|
+
}
|
|
2344
|
+
function emitDbQuery(cfg, sid, aid, payload) {
|
|
2345
|
+
if (!sid)
|
|
2346
|
+
return;
|
|
2347
|
+
const spanContext = payload?.spanContext ?? captureSpanContextFromTracer();
|
|
2348
|
+
if (!shouldCaptureDbSpan(spanContext))
|
|
2349
|
+
return;
|
|
2350
|
+
const dbEntry = attachSpanContext({
|
|
2351
|
+
collection: payload.collection,
|
|
2352
|
+
op: payload.op,
|
|
2353
|
+
query: payload.query ?? undefined,
|
|
2354
|
+
resultMeta: payload.resultMeta ?? undefined,
|
|
2355
|
+
durMs: payload.durMs ?? undefined,
|
|
2356
|
+
pk: null, before: null, after: null,
|
|
2357
|
+
error: payload.error ?? undefined,
|
|
2358
|
+
}, spanContext);
|
|
2359
|
+
post(cfg, sid, {
|
|
2360
|
+
entries: [{
|
|
2361
|
+
actionId: aid ?? null,
|
|
2362
|
+
db: [dbEntry],
|
|
2363
|
+
t: payload.t,
|
|
2364
|
+
}]
|
|
2365
|
+
});
|
|
2366
|
+
}
|
|
2367
|
+
function buildMinimalUpdate(before, after) {
|
|
2368
|
+
const set = {};
|
|
2369
|
+
const unset = {};
|
|
2370
|
+
function walk(b, a, path = '') {
|
|
2371
|
+
const bKeys = b ? Object.keys(b) : [];
|
|
2372
|
+
const aKeys = a ? Object.keys(a) : [];
|
|
2373
|
+
const all = new Set([...bKeys, ...aKeys]);
|
|
2374
|
+
for (const k of all) {
|
|
2375
|
+
const p = path ? `${path}.${k}` : k;
|
|
2376
|
+
const bv = b?.[k];
|
|
2377
|
+
const av = a?.[k];
|
|
2378
|
+
const bothObj = bv && av &&
|
|
2379
|
+
typeof bv === 'object' &&
|
|
2380
|
+
typeof av === 'object' &&
|
|
2381
|
+
!Array.isArray(bv) &&
|
|
2382
|
+
!Array.isArray(av);
|
|
2383
|
+
if (bothObj) {
|
|
2384
|
+
walk(bv, av, p);
|
|
2385
|
+
}
|
|
2386
|
+
else if (typeof av === 'undefined') {
|
|
2387
|
+
unset[p] = '';
|
|
2388
|
+
}
|
|
2389
|
+
else if (JSON.stringify(bv) !== JSON.stringify(av)) {
|
|
2390
|
+
set[p] = av;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
walk(before || {}, after || {});
|
|
2395
|
+
const update = {};
|
|
2396
|
+
if (Object.keys(set).length)
|
|
2397
|
+
update.$set = set;
|
|
2398
|
+
if (Object.keys(unset).length)
|
|
2399
|
+
update.$unset = unset;
|
|
2400
|
+
return update;
|
|
2401
|
+
}
|
|
2402
|
+
function patchSendgridMail(cfg) {
|
|
2403
|
+
let sgMail;
|
|
2404
|
+
try {
|
|
2405
|
+
sgMail = require('@sendgrid/mail');
|
|
2406
|
+
}
|
|
2407
|
+
catch {
|
|
2408
|
+
return;
|
|
2409
|
+
} // no-op if not installed
|
|
2410
|
+
if (!sgMail || sgMail.__repro_patched)
|
|
2411
|
+
return;
|
|
2412
|
+
sgMail.__repro_patched = true;
|
|
2413
|
+
const origSend = sgMail.send?.bind(sgMail);
|
|
2414
|
+
const origSendMultiple = sgMail.sendMultiple?.bind(sgMail);
|
|
2415
|
+
if (origSend) {
|
|
2416
|
+
sgMail.send = async function patchedSend(msg, isMultiple) {
|
|
2417
|
+
const t0 = Date.now();
|
|
2418
|
+
let statusCode;
|
|
2419
|
+
let headers;
|
|
2420
|
+
try {
|
|
2421
|
+
const res = await origSend(msg, isMultiple);
|
|
2422
|
+
const r = Array.isArray(res) ? res[0] : res;
|
|
2423
|
+
statusCode = r?.statusCode ?? r?.status;
|
|
2424
|
+
headers = r?.headers ?? undefined;
|
|
2425
|
+
return res;
|
|
2426
|
+
}
|
|
2427
|
+
finally {
|
|
2428
|
+
fireCapture('send', msg, t0, statusCode, headers);
|
|
2429
|
+
}
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
if (origSendMultiple) {
|
|
2433
|
+
sgMail.sendMultiple = async function patchedSendMultiple(msg) {
|
|
2434
|
+
const t0 = Date.now();
|
|
2435
|
+
let statusCode;
|
|
2436
|
+
let headers;
|
|
2437
|
+
try {
|
|
2438
|
+
const res = await origSendMultiple(msg);
|
|
2439
|
+
const r = Array.isArray(res) ? res[0] : res;
|
|
2440
|
+
statusCode = r?.statusCode ?? r?.status;
|
|
2441
|
+
headers = r?.headers ?? undefined;
|
|
2442
|
+
return res;
|
|
2443
|
+
}
|
|
2444
|
+
finally {
|
|
2445
|
+
fireCapture('sendMultiple', msg, t0, statusCode, headers);
|
|
2446
|
+
}
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2449
|
+
function fireCapture(kind, rawMsg, t0, statusCode, headers) {
|
|
2450
|
+
const ctx = getCtx();
|
|
2451
|
+
const sid = ctx.sid ?? cfg.resolveContext?.()?.sid;
|
|
2452
|
+
const aid = ctx.aid ?? cfg.resolveContext?.()?.aid;
|
|
2453
|
+
if (!sid)
|
|
2454
|
+
return;
|
|
2455
|
+
const norm = normalizeSendgridMessage(rawMsg);
|
|
2456
|
+
post(cfg, sid, {
|
|
2457
|
+
entries: [{
|
|
2458
|
+
actionId: aid ?? null,
|
|
2459
|
+
email: {
|
|
2460
|
+
provider: 'sendgrid',
|
|
2461
|
+
kind,
|
|
2462
|
+
to: norm.to, cc: norm.cc, bcc: norm.bcc, from: norm.from,
|
|
2463
|
+
subject: norm.subject, text: norm.text, html: norm.html,
|
|
2464
|
+
templateId: norm.templateId, dynamicTemplateData: norm.dynamicTemplateData,
|
|
2465
|
+
categories: norm.categories, customArgs: norm.customArgs,
|
|
2466
|
+
attachmentsMeta: norm.attachmentsMeta,
|
|
2467
|
+
statusCode, durMs: Date.now() - t0, headers: headers ?? {},
|
|
2468
|
+
},
|
|
2469
|
+
t: alignedNow(),
|
|
2470
|
+
}]
|
|
2471
|
+
});
|
|
2472
|
+
}
|
|
2473
|
+
function normalizeAddress(a) {
|
|
2474
|
+
if (!a)
|
|
2475
|
+
return null;
|
|
2476
|
+
if (typeof a === 'string')
|
|
2477
|
+
return { email: a };
|
|
2478
|
+
if (typeof a === 'object' && a.email)
|
|
2479
|
+
return { email: String(a.email), name: a.name ? String(a.name) : undefined };
|
|
2480
|
+
return null;
|
|
2481
|
+
}
|
|
2482
|
+
function normalizeAddressList(v) {
|
|
2483
|
+
if (!v)
|
|
2484
|
+
return undefined;
|
|
2485
|
+
const arr = Array.isArray(v) ? v : [v];
|
|
2486
|
+
const out = arr.map(normalizeAddress).filter(Boolean);
|
|
2487
|
+
return out.length ? out : undefined;
|
|
2488
|
+
}
|
|
2489
|
+
function normalizeSendgridMessage(msg) {
|
|
2490
|
+
const base = {
|
|
2491
|
+
from: normalizeAddress(msg?.from) ?? undefined,
|
|
2492
|
+
to: normalizeAddressList(msg?.to),
|
|
2493
|
+
cc: normalizeAddressList(msg?.cc),
|
|
2494
|
+
bcc: normalizeAddressList(msg?.bcc),
|
|
2495
|
+
subject: msg?.subject ? String(msg.subject) : undefined,
|
|
2496
|
+
text: typeof msg?.text === 'string' ? msg.text : undefined,
|
|
2497
|
+
html: typeof msg?.html === 'string' ? msg.html : undefined,
|
|
2498
|
+
templateId: msg?.templateId ? String(msg.templateId) : undefined,
|
|
2499
|
+
dynamicTemplateData: msg?.dynamic_template_data ?? msg?.dynamicTemplateData ?? undefined,
|
|
2500
|
+
categories: Array.isArray(msg?.categories) ? msg.categories.map(String) : undefined,
|
|
2501
|
+
customArgs: msg?.customArgs ?? msg?.custom_args ?? undefined,
|
|
2502
|
+
attachmentsMeta: Array.isArray(msg?.attachments)
|
|
2503
|
+
? msg.attachments.map((a) => ({
|
|
2504
|
+
filename: a?.filename ? String(a.filename) : undefined,
|
|
2505
|
+
type: a?.type ? String(a.type) : undefined,
|
|
2506
|
+
size: a?.content ? byteLen(a.content) : undefined,
|
|
2507
|
+
}))
|
|
2508
|
+
: undefined,
|
|
2509
|
+
};
|
|
2510
|
+
const p0 = Array.isArray(msg?.personalizations) ? msg.personalizations[0] : undefined;
|
|
2511
|
+
if (p0) {
|
|
2512
|
+
base.to = normalizeAddressList(p0.to) ?? base.to;
|
|
2513
|
+
base.cc = normalizeAddressList(p0.cc) ?? base.cc;
|
|
2514
|
+
base.bcc = normalizeAddressList(p0.bcc) ?? base.bcc;
|
|
2515
|
+
if (!base.subject && p0.subject)
|
|
2516
|
+
base.subject = String(p0.subject);
|
|
2517
|
+
if (!base.dynamicTemplateData && p0.dynamic_template_data)
|
|
2518
|
+
base.dynamicTemplateData = p0.dynamic_template_data;
|
|
2519
|
+
if (!base.customArgs && p0.custom_args)
|
|
2520
|
+
base.customArgs = p0.custom_args;
|
|
2521
|
+
}
|
|
2522
|
+
return base;
|
|
2523
|
+
}
|
|
2524
|
+
function byteLen(content) {
|
|
2525
|
+
try {
|
|
2526
|
+
if (typeof content === 'string')
|
|
2527
|
+
return Buffer.byteLength(content, 'utf8');
|
|
2528
|
+
if (content && typeof content === 'object' && 'length' in content)
|
|
2529
|
+
return Number(content.length);
|
|
2530
|
+
}
|
|
2531
|
+
catch { }
|
|
2532
|
+
return undefined;
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
exports.patchSendgridMail = patchSendgridMail;
|