@reproapp/node-sdk 0.0.38 → 0.0.41
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 +228 -310
- package/dist/index.d.ts +55 -0
- package/dist/index.js +1038 -21
- package/dist/ingest/client.js +2 -1
- package/dist/ingest/mapper.js +2 -0
- package/dist/ingest/types.d.ts +1 -0
- package/package.json +10 -4
- package/src/index.ts +1320 -20
- package/src/ingest/client.ts +1 -0
- package/src/ingest/mapper.ts +1 -0
- package/src/ingest/types.ts +1 -0
- package/test/backend-live-mode.test.js +421 -0
- package/test/postgresql-integration.test.js +302 -0
- package/test/postgresql-integrations.testcontainers.test.js +261 -0
- package/test/request-manifest-priority.test.js +6 -0
package/src/index.ts
CHANGED
|
@@ -323,6 +323,9 @@ const KAFKA_HEADER_SESSION_ID = 'x-bug-session-id';
|
|
|
323
323
|
const KAFKA_HEADER_ACTION_ID = 'x-bug-action-id';
|
|
324
324
|
const DEFAULT_INGEST_BASE = 'http://localhost:8080';
|
|
325
325
|
const DEFAULT_INGEST_PATH = '/v1/ingest/events';
|
|
326
|
+
const DEFAULT_LIVE_SESSION_DURATION_MS = 3 * 60_000;
|
|
327
|
+
const DEFAULT_LIVE_SEGMENT_IDLE_MS = 15_000;
|
|
328
|
+
const DEFAULT_LIVE_SEGMENT_DURATION_MS = DEFAULT_LIVE_SESSION_DURATION_MS;
|
|
326
329
|
|
|
327
330
|
function escapeRx(s: string) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
328
331
|
let __TRACER__: TracerApi | null = null;
|
|
@@ -1220,6 +1223,35 @@ function buildRequestCaptureManifestPayload(
|
|
|
1220
1223
|
};
|
|
1221
1224
|
}
|
|
1222
1225
|
|
|
1226
|
+
function emitRequestCaptureStart(
|
|
1227
|
+
cfg: IngestClientConfig,
|
|
1228
|
+
sessionId: string | null | undefined,
|
|
1229
|
+
actionId: string | null | undefined,
|
|
1230
|
+
scope: RequestCaptureScopeState,
|
|
1231
|
+
openedAtMs?: number | null,
|
|
1232
|
+
): void {
|
|
1233
|
+
if (!sessionId) {
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
const openedAt =
|
|
1237
|
+
typeof openedAtMs === 'number' && Number.isFinite(openedAtMs)
|
|
1238
|
+
? openedAtMs
|
|
1239
|
+
: alignedNow();
|
|
1240
|
+
post(cfg, sessionId, {
|
|
1241
|
+
entries: [{
|
|
1242
|
+
actionId: actionId ?? null,
|
|
1243
|
+
requestRid: scope.requestRid,
|
|
1244
|
+
requestCaptureStart: {
|
|
1245
|
+
scopeId: `req:${scope.requestRid}`,
|
|
1246
|
+
requestRid: scope.requestRid,
|
|
1247
|
+
transport: scope.transport,
|
|
1248
|
+
openedAt,
|
|
1249
|
+
},
|
|
1250
|
+
t: openedAt,
|
|
1251
|
+
}],
|
|
1252
|
+
}, { priority: 'high' });
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1223
1255
|
const als = new AsyncLocalStorage<Ctx>();
|
|
1224
1256
|
const getCtx = () => als.getStore() || {};
|
|
1225
1257
|
|
|
@@ -2078,6 +2110,397 @@ function toIngestConfig(cfg: IngestClientConfig): IngestClientConfig {
|
|
|
2078
2110
|
};
|
|
2079
2111
|
}
|
|
2080
2112
|
|
|
2113
|
+
function normalizeLiveModeConfig(cfg: ReproMiddlewareConfig): ReproLiveModeConfig | null {
|
|
2114
|
+
const liveMode = cfg.liveMode;
|
|
2115
|
+
if (liveMode === true) {
|
|
2116
|
+
return { enabled: true };
|
|
2117
|
+
}
|
|
2118
|
+
if (!liveMode || typeof liveMode !== 'object') {
|
|
2119
|
+
return null;
|
|
2120
|
+
}
|
|
2121
|
+
if (liveMode.enabled === false) {
|
|
2122
|
+
return null;
|
|
2123
|
+
}
|
|
2124
|
+
return liveMode;
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
type BackendLiveTimingConfig = {
|
|
2128
|
+
sessionMaxDurationMs: number;
|
|
2129
|
+
segmentMaxDurationMs: number;
|
|
2130
|
+
segmentIdleMs: number;
|
|
2131
|
+
};
|
|
2132
|
+
|
|
2133
|
+
let backendLiveTimingOverrideForTest: Partial<BackendLiveTimingConfig> | null = null;
|
|
2134
|
+
|
|
2135
|
+
function positiveInternalDuration(value: unknown, fallback: number): number {
|
|
2136
|
+
const numeric = Number(value);
|
|
2137
|
+
if (!Number.isFinite(numeric) || numeric <= 0) return fallback;
|
|
2138
|
+
return Math.max(1, Math.trunc(numeric));
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
function getBackendLiveTiming(): BackendLiveTimingConfig {
|
|
2142
|
+
return {
|
|
2143
|
+
sessionMaxDurationMs: positiveInternalDuration(
|
|
2144
|
+
backendLiveTimingOverrideForTest?.sessionMaxDurationMs,
|
|
2145
|
+
DEFAULT_LIVE_SESSION_DURATION_MS,
|
|
2146
|
+
),
|
|
2147
|
+
segmentMaxDurationMs: positiveInternalDuration(
|
|
2148
|
+
backendLiveTimingOverrideForTest?.segmentMaxDurationMs,
|
|
2149
|
+
DEFAULT_LIVE_SEGMENT_DURATION_MS,
|
|
2150
|
+
),
|
|
2151
|
+
segmentIdleMs: positiveInternalDuration(
|
|
2152
|
+
backendLiveTimingOverrideForTest?.segmentIdleMs,
|
|
2153
|
+
DEFAULT_LIVE_SEGMENT_IDLE_MS,
|
|
2154
|
+
),
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
function normalizeLiveActorId(value: unknown): string | null {
|
|
2159
|
+
if (value === undefined || value === null) return null;
|
|
2160
|
+
const normalized = String(value).trim();
|
|
2161
|
+
return normalized.length ? normalized : null;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
function normalizeLiveActorLabels(
|
|
2165
|
+
value: Record<string, string | number | boolean | null | undefined> | null | undefined,
|
|
2166
|
+
): Record<string, string> | undefined {
|
|
2167
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
2168
|
+
const labels: Record<string, string> = {};
|
|
2169
|
+
for (const [key, rawValue] of Object.entries(value)) {
|
|
2170
|
+
const normalizedKey = key.trim();
|
|
2171
|
+
if (!normalizedKey || rawValue === undefined || rawValue === null) continue;
|
|
2172
|
+
const normalizedValue = String(rawValue).trim();
|
|
2173
|
+
if (normalizedValue) labels[normalizedKey] = normalizedValue;
|
|
2174
|
+
}
|
|
2175
|
+
return Object.keys(labels).length ? labels : undefined;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
function newSessionId(base: number = Date.now()): string {
|
|
2179
|
+
return `S_${Math.max(0, Math.trunc(base))}_${randomUUID().slice(0, 8)}`;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
function newActionId(base: number = Date.now()): string {
|
|
2183
|
+
return `A_${Math.max(0, Math.trunc(base))}_${randomUUID().slice(0, 8)}`;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
function resolveLiveApiBase(cfg: ReproMiddlewareConfig, liveMode?: ReproLiveModeConfig | null): string | null {
|
|
2187
|
+
const env = typeof process !== 'undefined' ? (process as any)?.env : undefined;
|
|
2188
|
+
const raw =
|
|
2189
|
+
liveMode?.apiBase ||
|
|
2190
|
+
cfg.apiBase ||
|
|
2191
|
+
env?.REPRO_API_BASE_URL ||
|
|
2192
|
+
env?.REPRO_API_BASE ||
|
|
2193
|
+
(cfg.privacy as RuntimePrivacyConfig | undefined)?.apiBase ||
|
|
2194
|
+
null;
|
|
2195
|
+
if (typeof raw !== 'string' || !raw.trim()) return null;
|
|
2196
|
+
return raw.trim().replace(/\/+$/, '');
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
function liveApiHeaders(cfg: ReproMiddlewareConfig): Record<string, string> {
|
|
2200
|
+
const headers: Record<string, string> = {
|
|
2201
|
+
'Content-Type': 'application/json',
|
|
2202
|
+
'X-App-Id': cfg.appId,
|
|
2203
|
+
};
|
|
2204
|
+
if (cfg.appSecret) {
|
|
2205
|
+
headers['X-App-Secret'] = cfg.appSecret;
|
|
2206
|
+
}
|
|
2207
|
+
if (cfg.appName) {
|
|
2208
|
+
headers['X-App-Name'] = cfg.appName;
|
|
2209
|
+
}
|
|
2210
|
+
return headers;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
async function postLiveApi(
|
|
2214
|
+
cfg: ReproMiddlewareConfig,
|
|
2215
|
+
liveMode: ReproLiveModeConfig | null,
|
|
2216
|
+
path: string,
|
|
2217
|
+
body: Record<string, any>,
|
|
2218
|
+
): Promise<void> {
|
|
2219
|
+
await postLiveApiJson(cfg, liveMode, path, body);
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
async function postLiveApiJson<T = Record<string, any>>(
|
|
2223
|
+
cfg: ReproMiddlewareConfig,
|
|
2224
|
+
liveMode: ReproLiveModeConfig | null,
|
|
2225
|
+
path: string,
|
|
2226
|
+
body: Record<string, any>,
|
|
2227
|
+
): Promise<T | null> {
|
|
2228
|
+
const apiBase = resolveLiveApiBase(cfg, liveMode);
|
|
2229
|
+
const fetchImpl = (globalThis as any).fetch;
|
|
2230
|
+
if (!apiBase || !cfg.appId || !cfg.appSecret || typeof fetchImpl !== 'function') {
|
|
2231
|
+
return null;
|
|
2232
|
+
}
|
|
2233
|
+
try {
|
|
2234
|
+
const res = await fetchImpl(`${apiBase}${path}`, {
|
|
2235
|
+
method: 'POST',
|
|
2236
|
+
headers: liveApiHeaders(cfg),
|
|
2237
|
+
body: JSON.stringify(body),
|
|
2238
|
+
});
|
|
2239
|
+
if (!res?.ok) {
|
|
2240
|
+
return null;
|
|
2241
|
+
}
|
|
2242
|
+
try {
|
|
2243
|
+
return await res.json();
|
|
2244
|
+
} catch {
|
|
2245
|
+
return null;
|
|
2246
|
+
}
|
|
2247
|
+
} catch {
|
|
2248
|
+
// never break user code
|
|
2249
|
+
return null;
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
function enqueueLiveSessionStart(
|
|
2254
|
+
cfg: ReproMiddlewareConfig,
|
|
2255
|
+
liveMode: ReproLiveModeConfig | null,
|
|
2256
|
+
payload: Record<string, any>,
|
|
2257
|
+
): void {
|
|
2258
|
+
scheduleSdkBackgroundWork(
|
|
2259
|
+
() => postLiveApi(cfg, liveMode, '/v1/live-sessions', payload),
|
|
2260
|
+
{ priority: 'high' },
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
function enqueueLiveSessionFinish(
|
|
2265
|
+
cfg: ReproMiddlewareConfig,
|
|
2266
|
+
liveMode: ReproLiveModeConfig | null,
|
|
2267
|
+
sessionId: string,
|
|
2268
|
+
): void {
|
|
2269
|
+
scheduleSdkBackgroundWork(
|
|
2270
|
+
() => postLiveApi(
|
|
2271
|
+
cfg,
|
|
2272
|
+
liveMode,
|
|
2273
|
+
`/v1/live-sessions/${encodeURIComponent(sessionId)}/finish`,
|
|
2274
|
+
{ notes: '' },
|
|
2275
|
+
),
|
|
2276
|
+
{ priority: 'high' },
|
|
2277
|
+
);
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
function emitBackendLiveSessionManifest(
|
|
2281
|
+
cfg: ReproMiddlewareConfig,
|
|
2282
|
+
session: BackendLiveParentSession | BackendLiveChildSession,
|
|
2283
|
+
options: {
|
|
2284
|
+
sessionKind: 'parent' | 'child';
|
|
2285
|
+
actorId: string;
|
|
2286
|
+
actorType: string;
|
|
2287
|
+
actorLabels?: Record<string, string>;
|
|
2288
|
+
parentSessionId?: string | null;
|
|
2289
|
+
previousSessionId?: string | null;
|
|
2290
|
+
closedAt: number;
|
|
2291
|
+
},
|
|
2292
|
+
): void {
|
|
2293
|
+
post(cfg, session.sessionId, {
|
|
2294
|
+
entries: [{
|
|
2295
|
+
sessionCaptureManifest: {
|
|
2296
|
+
mode: 'backend-only',
|
|
2297
|
+
captureMode: 'backend-only',
|
|
2298
|
+
sessionKind: options.sessionKind,
|
|
2299
|
+
parentSessionId: options.parentSessionId ?? null,
|
|
2300
|
+
previousSessionId: options.previousSessionId ?? null,
|
|
2301
|
+
actorId: options.actorId,
|
|
2302
|
+
actorType: options.actorType,
|
|
2303
|
+
actorLabels: options.actorLabels,
|
|
2304
|
+
expected: {
|
|
2305
|
+
frontendActionDoc: 0,
|
|
2306
|
+
rrwebChunk: 0,
|
|
2307
|
+
frontendSessionEnd: 0,
|
|
2308
|
+
},
|
|
2309
|
+
closedAt: options.closedAt,
|
|
2310
|
+
},
|
|
2311
|
+
t: options.closedAt,
|
|
2312
|
+
}],
|
|
2313
|
+
}, { priority: 'high' });
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
function closeBackendLiveParent(
|
|
2317
|
+
cfg: ReproMiddlewareConfig,
|
|
2318
|
+
liveMode: ReproLiveModeConfig | null,
|
|
2319
|
+
state: BackendLiveActorState,
|
|
2320
|
+
parent: BackendLiveParentSession,
|
|
2321
|
+
closedAt: number,
|
|
2322
|
+
): void {
|
|
2323
|
+
if (parent.closed) {
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
parent.closed = true;
|
|
2327
|
+
if (parent.closeTimer) {
|
|
2328
|
+
try { clearTimeout(parent.closeTimer); } catch {}
|
|
2329
|
+
parent.closeTimer = null;
|
|
2330
|
+
}
|
|
2331
|
+
state.previousParentSessionId = parent.sessionId;
|
|
2332
|
+
if (state.parent === parent) {
|
|
2333
|
+
state.parent = null;
|
|
2334
|
+
}
|
|
2335
|
+
emitBackendLiveSessionManifest(cfg, parent, {
|
|
2336
|
+
sessionKind: 'parent',
|
|
2337
|
+
actorId: parent.actorId,
|
|
2338
|
+
actorType: parent.actorType,
|
|
2339
|
+
actorLabels: parent.actorLabels,
|
|
2340
|
+
previousSessionId: parent.previousSessionId,
|
|
2341
|
+
closedAt,
|
|
2342
|
+
});
|
|
2343
|
+
enqueueLiveSessionFinish(cfg, liveMode, parent.sessionId);
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
function scheduleBackendLiveParentClose(
|
|
2347
|
+
cfg: ReproMiddlewareConfig,
|
|
2348
|
+
liveMode: ReproLiveModeConfig | null,
|
|
2349
|
+
state: BackendLiveActorState,
|
|
2350
|
+
parent: BackendLiveParentSession,
|
|
2351
|
+
): void {
|
|
2352
|
+
if (parent.closed) {
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
if (parent.closeTimer) {
|
|
2356
|
+
try { clearTimeout(parent.closeTimer); } catch {}
|
|
2357
|
+
parent.closeTimer = null;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
const now = Date.now();
|
|
2361
|
+
const activeChild = state.activeChild;
|
|
2362
|
+
const activeChildBelongsToParent =
|
|
2363
|
+
activeChild?.parentSessionId === parent.sessionId;
|
|
2364
|
+
const closeAt =
|
|
2365
|
+
activeChildBelongsToParent && now >= parent.expiresAt
|
|
2366
|
+
? Math.max(parent.expiresAt, activeChild.closeDeadline, now + 1)
|
|
2367
|
+
: parent.expiresAt;
|
|
2368
|
+
|
|
2369
|
+
parent.closeTimer = setTimeout(() => {
|
|
2370
|
+
parent.closeTimer = null;
|
|
2371
|
+
if (parent.closed || state.parent !== parent) {
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
const currentChild = state.activeChild;
|
|
2376
|
+
if (currentChild?.parentSessionId === parent.sessionId) {
|
|
2377
|
+
if (currentChild.requestsInFlight <= 0) {
|
|
2378
|
+
scheduleBackendLiveChildClose(cfg, liveMode, state, currentChild);
|
|
2379
|
+
}
|
|
2380
|
+
scheduleBackendLiveParentClose(cfg, liveMode, state, parent);
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
closeBackendLiveParent(cfg, liveMode, state, parent, Date.now());
|
|
2385
|
+
}, Math.max(0, closeAt - now));
|
|
2386
|
+
try { (parent.closeTimer as any).unref?.(); } catch {}
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
function scheduleBackendLiveChildClose(
|
|
2390
|
+
cfg: ReproMiddlewareConfig,
|
|
2391
|
+
liveMode: ReproLiveModeConfig | null,
|
|
2392
|
+
state: BackendLiveActorState,
|
|
2393
|
+
child: BackendLiveChildSession,
|
|
2394
|
+
): void {
|
|
2395
|
+
if (child.closeTimer) {
|
|
2396
|
+
try { clearTimeout(child.closeTimer); } catch {}
|
|
2397
|
+
child.closeTimer = null;
|
|
2398
|
+
}
|
|
2399
|
+
const delay = Math.max(0, child.closeDeadline - Date.now());
|
|
2400
|
+
child.closeTimer = setTimeout(() => {
|
|
2401
|
+
child.closeTimer = null;
|
|
2402
|
+
if (child.requestsInFlight > 0 || Date.now() < child.closeDeadline) {
|
|
2403
|
+
scheduleBackendLiveChildClose(cfg, liveMode, state, child);
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
if (state.activeChild === child) {
|
|
2407
|
+
state.activeChild = null;
|
|
2408
|
+
}
|
|
2409
|
+
state.previousChildSessionId = child.sessionId;
|
|
2410
|
+
const closedAt = Date.now();
|
|
2411
|
+
emitBackendLiveSessionManifest(cfg, child, {
|
|
2412
|
+
sessionKind: 'child',
|
|
2413
|
+
actorId: child.actorId,
|
|
2414
|
+
actorType: child.actorType,
|
|
2415
|
+
actorLabels: child.actorLabels,
|
|
2416
|
+
parentSessionId: child.parentSessionId,
|
|
2417
|
+
previousSessionId: child.previousSessionId,
|
|
2418
|
+
closedAt,
|
|
2419
|
+
});
|
|
2420
|
+
enqueueLiveSessionFinish(cfg, liveMode, child.sessionId);
|
|
2421
|
+
}, delay);
|
|
2422
|
+
try { (child.closeTimer as any).unref?.(); } catch {}
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
function releaseBackendLiveRequest(
|
|
2426
|
+
cfg: ReproMiddlewareConfig,
|
|
2427
|
+
liveMode: ReproLiveModeConfig | null,
|
|
2428
|
+
state: BackendLiveActorState,
|
|
2429
|
+
child: BackendLiveChildSession,
|
|
2430
|
+
finishedAt: number,
|
|
2431
|
+
): void {
|
|
2432
|
+
child.requestsInFlight = Math.max(0, child.requestsInFlight - 1);
|
|
2433
|
+
const idleMs = getBackendLiveTiming().segmentIdleMs;
|
|
2434
|
+
child.closeDeadline = Math.max(child.closeDeadline, finishedAt + idleMs);
|
|
2435
|
+
if (child.requestsInFlight <= 0) {
|
|
2436
|
+
scheduleBackendLiveChildClose(cfg, liveMode, state, child);
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
async function resolveBackendLiveContext(
|
|
2441
|
+
cfg: ReproMiddlewareConfig,
|
|
2442
|
+
req: Request,
|
|
2443
|
+
requestStart: number,
|
|
2444
|
+
): Promise<BackendLiveResolvedContext | null> {
|
|
2445
|
+
const liveMode = normalizeLiveModeConfig(cfg);
|
|
2446
|
+
if (!liveMode?.backendOnly) {
|
|
2447
|
+
return null;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
const method = String(req.method || 'GET').toUpperCase();
|
|
2451
|
+
if (method === 'OPTIONS' && !hasReproRequestCorrelation(req)) {
|
|
2452
|
+
return null;
|
|
2453
|
+
}
|
|
2454
|
+
const identity = await resolveLiveActorIdentity(cfg, req);
|
|
2455
|
+
if (!identity) {
|
|
2456
|
+
return null;
|
|
2457
|
+
}
|
|
2458
|
+
const { actorId, actorType, actorLabels } = identity;
|
|
2459
|
+
|
|
2460
|
+
const claim = await postLiveApiJson<BackendLiveClaimResponse>(
|
|
2461
|
+
cfg,
|
|
2462
|
+
liveMode,
|
|
2463
|
+
'/v1/live-sessions/claim',
|
|
2464
|
+
{
|
|
2465
|
+
clientTime: requestStart,
|
|
2466
|
+
captureMode: 'backend-only',
|
|
2467
|
+
actorId,
|
|
2468
|
+
actorIds: [actorId],
|
|
2469
|
+
actorType,
|
|
2470
|
+
actorLabels,
|
|
2471
|
+
},
|
|
2472
|
+
);
|
|
2473
|
+
const sid =
|
|
2474
|
+
normalizeLiveActorId(claim?.childSessionId) ??
|
|
2475
|
+
normalizeLiveActorId(claim?.sessionId);
|
|
2476
|
+
if (!sid) {
|
|
2477
|
+
return null;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
const aid = normalizeLiveActorId(claim?.actionId) ?? newActionId(requestStart);
|
|
2481
|
+
const parentSessionId = normalizeLiveActorId(claim?.parentSessionId) ?? '';
|
|
2482
|
+
const liveChild: BackendLiveChildSession = {
|
|
2483
|
+
sessionId: sid,
|
|
2484
|
+
parentSessionId,
|
|
2485
|
+
previousSessionId: null,
|
|
2486
|
+
actorId,
|
|
2487
|
+
actorType,
|
|
2488
|
+
actorLabels,
|
|
2489
|
+
startedAt: requestStart,
|
|
2490
|
+
expiresAt: requestStart,
|
|
2491
|
+
closeDeadline: requestStart,
|
|
2492
|
+
requestsInFlight: 0,
|
|
2493
|
+
closeTimer: null,
|
|
2494
|
+
};
|
|
2495
|
+
|
|
2496
|
+
return {
|
|
2497
|
+
sid: liveChild.sessionId,
|
|
2498
|
+
aid,
|
|
2499
|
+
requestStart,
|
|
2500
|
+
child: liveChild,
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2081
2504
|
function resolveIngestUrl(cfg: IngestClientConfig): string {
|
|
2082
2505
|
const envBase = typeof process !== 'undefined' ? (process as any)?.env?.REPRO_INGEST_BASE : undefined;
|
|
2083
2506
|
const ingestBase = String(envBase || cfg.ingestBase || DEFAULT_INGEST_BASE).replace(/\/+$/, '');
|
|
@@ -2125,6 +2548,24 @@ function readRequestQueryNumber(req: Request, name: string): number | null {
|
|
|
2125
2548
|
return Number.isFinite(parsed) ? parsed : null;
|
|
2126
2549
|
}
|
|
2127
2550
|
|
|
2551
|
+
function hasReproRequestCorrelation(req: Request): boolean {
|
|
2552
|
+
const headers = req.headers || {};
|
|
2553
|
+
if (
|
|
2554
|
+
hasHttpHeader(headers, KAFKA_HEADER_SESSION_ID) ||
|
|
2555
|
+
hasHttpHeader(headers, KAFKA_HEADER_ACTION_ID) ||
|
|
2556
|
+
hasHttpHeader(headers, KAFKA_HEADER_REQUEST_ID) ||
|
|
2557
|
+
hasHttpHeader(headers, KAFKA_HEADER_TRACE_ID) ||
|
|
2558
|
+
hasHttpHeader(headers, KAFKA_HEADER_SPAN_ID) ||
|
|
2559
|
+
hasHttpHeader(headers, KAFKA_HEADER_PARENT_SPAN_ID)
|
|
2560
|
+
) {
|
|
2561
|
+
return true;
|
|
2562
|
+
}
|
|
2563
|
+
return Boolean(
|
|
2564
|
+
readRequestQueryParam(req, REPRO_QUERY_SESSION_PARAM) ||
|
|
2565
|
+
readRequestQueryParam(req, REPRO_QUERY_ACTION_PARAM),
|
|
2566
|
+
);
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2128
2569
|
function stripReproQueryFromUrl(rawUrl: string): string {
|
|
2129
2570
|
if (!rawUrl || !rawUrl.includes('?')) {
|
|
2130
2571
|
return rawUrl;
|
|
@@ -5008,14 +5449,193 @@ async function maybeCaptureDbValueFromCandidatesAsync(
|
|
|
5008
5449
|
// reproMiddleware — unchanged behavior + passive per-request trace
|
|
5009
5450
|
// ===================================================================
|
|
5010
5451
|
export type ReproMiddlewareConfig = IngestClientConfig & {
|
|
5452
|
+
/** API base used for backend-owned live session documents. Defaults to REPRO_API_BASE_URL/REPRO_API_BASE/privacy.apiBase. */
|
|
5453
|
+
apiBase?: string;
|
|
5011
5454
|
/** Configure header capture/masking. Defaults to capturing with sensitive headers masked. */
|
|
5012
5455
|
captureHeaders?: boolean | HeaderCaptureOptions;
|
|
5013
5456
|
/** Optional masking rules for request/response payloads and function traces. */
|
|
5014
5457
|
masking?: ReproMaskingConfig;
|
|
5015
5458
|
/** Optional privacy policy config for deterministic runtime transformations. */
|
|
5016
5459
|
privacy?: RuntimePrivacyConfig;
|
|
5460
|
+
/** Opt-in production/live capture mode. Staging/manual tagged capture is unchanged when disabled. */
|
|
5461
|
+
liveMode?: ReproLiveModeConfig | boolean;
|
|
5017
5462
|
};
|
|
5018
5463
|
|
|
5464
|
+
export type ReproLiveActorArgs = {
|
|
5465
|
+
req: Request;
|
|
5466
|
+
headers: Request['headers'];
|
|
5467
|
+
method: string;
|
|
5468
|
+
url: string;
|
|
5469
|
+
path: string;
|
|
5470
|
+
ip?: string | null;
|
|
5471
|
+
appId: string;
|
|
5472
|
+
appName?: string;
|
|
5473
|
+
serviceName?: string;
|
|
5474
|
+
};
|
|
5475
|
+
|
|
5476
|
+
export type ReproLiveActorDecision = ReproLiveActorArgs & {
|
|
5477
|
+
actorId: string;
|
|
5478
|
+
actorType?: string;
|
|
5479
|
+
actorLabels?: Record<string, string>;
|
|
5480
|
+
};
|
|
5481
|
+
|
|
5482
|
+
export type ReproLiveModeConfig = {
|
|
5483
|
+
enabled?: boolean;
|
|
5484
|
+
backendOnly?: boolean;
|
|
5485
|
+
/**
|
|
5486
|
+
* Stable actor/correlation key for live capture. Return null/undefined/empty
|
|
5487
|
+
* to skip capture for that request.
|
|
5488
|
+
*/
|
|
5489
|
+
defineActorId?: (
|
|
5490
|
+
args: ReproLiveActorArgs,
|
|
5491
|
+
) => string | number | null | undefined | Promise<string | number | null | undefined>;
|
|
5492
|
+
/**
|
|
5493
|
+
* Optional filter for segmenting which actors/requests are captured.
|
|
5494
|
+
*/
|
|
5495
|
+
shouldCapture?: (
|
|
5496
|
+
args: ReproLiveActorDecision,
|
|
5497
|
+
) => boolean | Promise<boolean>;
|
|
5498
|
+
actorType?: string;
|
|
5499
|
+
actorLabels?:
|
|
5500
|
+
| Record<string, string | number | boolean | null | undefined>
|
|
5501
|
+
| ((args: ReproLiveActorDecision) =>
|
|
5502
|
+
| Record<string, string | number | boolean | null | undefined>
|
|
5503
|
+
| null
|
|
5504
|
+
| undefined
|
|
5505
|
+
| Promise<Record<string, string | number | boolean | null | undefined> | null | undefined>);
|
|
5506
|
+
apiBase?: string;
|
|
5507
|
+
};
|
|
5508
|
+
|
|
5509
|
+
type BackendLiveParentSession = {
|
|
5510
|
+
sessionId: string;
|
|
5511
|
+
startedAt: number;
|
|
5512
|
+
expiresAt: number;
|
|
5513
|
+
previousSessionId: string | null;
|
|
5514
|
+
actorId: string;
|
|
5515
|
+
actorType: string;
|
|
5516
|
+
actorLabels?: Record<string, string>;
|
|
5517
|
+
closeTimer?: NodeJS.Timeout | null;
|
|
5518
|
+
closed?: boolean;
|
|
5519
|
+
};
|
|
5520
|
+
|
|
5521
|
+
type BackendLiveChildSession = {
|
|
5522
|
+
sessionId: string;
|
|
5523
|
+
parentSessionId: string;
|
|
5524
|
+
previousSessionId: string | null;
|
|
5525
|
+
actorId: string;
|
|
5526
|
+
actorType: string;
|
|
5527
|
+
actorLabels?: Record<string, string>;
|
|
5528
|
+
startedAt: number;
|
|
5529
|
+
expiresAt: number;
|
|
5530
|
+
closeDeadline: number;
|
|
5531
|
+
requestsInFlight: number;
|
|
5532
|
+
closeTimer?: NodeJS.Timeout | null;
|
|
5533
|
+
};
|
|
5534
|
+
|
|
5535
|
+
type BackendLiveActorState = {
|
|
5536
|
+
actorId: string;
|
|
5537
|
+
parent: BackendLiveParentSession | null;
|
|
5538
|
+
previousParentSessionId: string | null;
|
|
5539
|
+
activeChild: BackendLiveChildSession | null;
|
|
5540
|
+
previousChildSessionId: string | null;
|
|
5541
|
+
};
|
|
5542
|
+
|
|
5543
|
+
type BackendLiveActorIdentity = {
|
|
5544
|
+
actorId: string;
|
|
5545
|
+
actorType: string;
|
|
5546
|
+
actorLabels?: Record<string, string>;
|
|
5547
|
+
};
|
|
5548
|
+
|
|
5549
|
+
type BackendLiveResolvedContext = {
|
|
5550
|
+
sid: string;
|
|
5551
|
+
aid: string;
|
|
5552
|
+
requestStart: number;
|
|
5553
|
+
child: BackendLiveChildSession;
|
|
5554
|
+
};
|
|
5555
|
+
|
|
5556
|
+
type BackendLiveClaimResponse = {
|
|
5557
|
+
parentSessionId?: string | null;
|
|
5558
|
+
childSessionId?: string | null;
|
|
5559
|
+
sessionId?: string | null;
|
|
5560
|
+
actionId?: string | null;
|
|
5561
|
+
clockOffsetMs?: number | null;
|
|
5562
|
+
};
|
|
5563
|
+
|
|
5564
|
+
function buildLiveActorArgs(cfg: ReproMiddlewareConfig, req: Request): ReproLiveActorArgs {
|
|
5565
|
+
const rawUrl = String((req as any).originalUrl || req.url || '/');
|
|
5566
|
+
const path = rawUrl.split('?')[0] || '/';
|
|
5567
|
+
return {
|
|
5568
|
+
req,
|
|
5569
|
+
headers: req.headers,
|
|
5570
|
+
method: String(req.method || 'GET').toUpperCase(),
|
|
5571
|
+
url: rawUrl,
|
|
5572
|
+
path,
|
|
5573
|
+
ip: (req as any).ip ?? (req as any).socket?.remoteAddress ?? null,
|
|
5574
|
+
appId: cfg.appId,
|
|
5575
|
+
appName: cfg.appName,
|
|
5576
|
+
serviceName: cfg.serviceName,
|
|
5577
|
+
};
|
|
5578
|
+
}
|
|
5579
|
+
|
|
5580
|
+
async function resolveLiveActorIdentity(
|
|
5581
|
+
cfg: ReproMiddlewareConfig,
|
|
5582
|
+
req: Request,
|
|
5583
|
+
): Promise<BackendLiveActorIdentity | null> {
|
|
5584
|
+
const liveMode = normalizeLiveModeConfig(cfg);
|
|
5585
|
+
if (!liveMode) {
|
|
5586
|
+
return null;
|
|
5587
|
+
}
|
|
5588
|
+
|
|
5589
|
+
const args = buildLiveActorArgs(cfg, req);
|
|
5590
|
+
let rawActorId: string | number | null | undefined;
|
|
5591
|
+
try {
|
|
5592
|
+
rawActorId = liveMode.defineActorId
|
|
5593
|
+
? await liveMode.defineActorId(args)
|
|
5594
|
+
: cfg.actorId;
|
|
5595
|
+
} catch {
|
|
5596
|
+
rawActorId = null;
|
|
5597
|
+
}
|
|
5598
|
+
const actorId = normalizeLiveActorId(rawActorId);
|
|
5599
|
+
if (!actorId) {
|
|
5600
|
+
return null;
|
|
5601
|
+
}
|
|
5602
|
+
|
|
5603
|
+
const actorType = normalizeLiveActorId(liveMode.actorType) ?? cfg.actorType ?? 'user';
|
|
5604
|
+
const decision: ReproLiveActorDecision = {
|
|
5605
|
+
...args,
|
|
5606
|
+
actorId,
|
|
5607
|
+
actorType,
|
|
5608
|
+
};
|
|
5609
|
+
if (liveMode.shouldCapture) {
|
|
5610
|
+
let shouldCapture = false;
|
|
5611
|
+
try {
|
|
5612
|
+
shouldCapture = (await liveMode.shouldCapture(decision)) === true;
|
|
5613
|
+
} catch {
|
|
5614
|
+
shouldCapture = false;
|
|
5615
|
+
}
|
|
5616
|
+
if (!shouldCapture) {
|
|
5617
|
+
return null;
|
|
5618
|
+
}
|
|
5619
|
+
}
|
|
5620
|
+
|
|
5621
|
+
let rawLabels:
|
|
5622
|
+
| Record<string, string | number | boolean | null | undefined>
|
|
5623
|
+
| null
|
|
5624
|
+
| undefined;
|
|
5625
|
+
try {
|
|
5626
|
+
rawLabels =
|
|
5627
|
+
typeof liveMode.actorLabels === 'function'
|
|
5628
|
+
? await liveMode.actorLabels(decision)
|
|
5629
|
+
: liveMode.actorLabels;
|
|
5630
|
+
} catch {
|
|
5631
|
+
rawLabels = undefined;
|
|
5632
|
+
}
|
|
5633
|
+
const actorLabels = normalizeLiveActorLabels(rawLabels) ?? normalizeLiveActorLabels(cfg.actorLabels);
|
|
5634
|
+
return { actorId, actorType, actorLabels };
|
|
5635
|
+
}
|
|
5636
|
+
|
|
5637
|
+
const backendLiveActors = new Map<string, BackendLiveActorState>();
|
|
5638
|
+
|
|
5019
5639
|
export type ReproInitConfig = ReproMiddlewareConfig & {
|
|
5020
5640
|
/**
|
|
5021
5641
|
* Tracing configuration for function/http instrumentation.
|
|
@@ -5060,22 +5680,68 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5060
5680
|
endClientRequest();
|
|
5061
5681
|
};
|
|
5062
5682
|
|
|
5063
|
-
const
|
|
5064
|
-
const
|
|
5683
|
+
const requestStartRaw = Date.now();
|
|
5684
|
+
const headerTs = readHeaderNumber(req.headers[REQUEST_START_HEADER]) ?? queryTs;
|
|
5685
|
+
const requestEpochMs = headerTs !== null ? headerTs : requestStartRaw;
|
|
5686
|
+
let backendLiveState: BackendLiveActorState | null = null;
|
|
5687
|
+
let backendLiveChild: BackendLiveChildSession | null = null;
|
|
5688
|
+
const headerSid = (req.headers['x-bug-session-id'] as string) || '';
|
|
5689
|
+
const headerAid = (req.headers['x-bug-action-id'] as string) || '';
|
|
5690
|
+
const hasIncomingReproSession = Boolean(headerSid || querySid || headerAid || queryAid);
|
|
5691
|
+
const backendLiveContext =
|
|
5692
|
+
!hasIncomingReproSession
|
|
5693
|
+
? await resolveBackendLiveContext(cfg, req, requestEpochMs)
|
|
5694
|
+
: null;
|
|
5695
|
+
const taggedActorIdentity = hasIncomingReproSession
|
|
5696
|
+
? await resolveLiveActorIdentity(cfg, req)
|
|
5697
|
+
: null;
|
|
5698
|
+
if (backendLiveContext) {
|
|
5699
|
+
backendLiveState = backendLiveActors.get(backendLiveContext.child.actorId) ?? null;
|
|
5700
|
+
backendLiveChild = backendLiveContext.child;
|
|
5701
|
+
}
|
|
5702
|
+
const sid = headerSid || querySid || backendLiveContext?.sid || '';
|
|
5703
|
+
const aid = headerAid || queryAid || backendLiveContext?.aid || '';
|
|
5065
5704
|
if (!sid || !aid) {
|
|
5066
5705
|
res.on('finish', endTrackedClientRequest);
|
|
5067
5706
|
res.on('close', endTrackedClientRequest);
|
|
5068
5707
|
return next(); // only capture tagged requests
|
|
5069
5708
|
}
|
|
5709
|
+
const captureCfg: ReproMiddlewareConfig = backendLiveChild
|
|
5710
|
+
? {
|
|
5711
|
+
...cfg,
|
|
5712
|
+
actorId: backendLiveChild.actorId,
|
|
5713
|
+
actorType: backendLiveChild.actorType,
|
|
5714
|
+
actorLabels: backendLiveChild.actorLabels,
|
|
5715
|
+
}
|
|
5716
|
+
: taggedActorIdentity
|
|
5717
|
+
? {
|
|
5718
|
+
...cfg,
|
|
5719
|
+
actorId: taggedActorIdentity.actorId,
|
|
5720
|
+
actorType: taggedActorIdentity.actorType,
|
|
5721
|
+
actorLabels: taggedActorIdentity.actorLabels,
|
|
5722
|
+
}
|
|
5723
|
+
: cfg;
|
|
5724
|
+
if (taggedActorIdentity) {
|
|
5725
|
+
post(captureCfg, sid, {
|
|
5726
|
+
entries: [{
|
|
5727
|
+
actionId: aid,
|
|
5728
|
+
sessionActor: {
|
|
5729
|
+
actorId: taggedActorIdentity.actorId,
|
|
5730
|
+
actorType: taggedActorIdentity.actorType,
|
|
5731
|
+
actorLabels: taggedActorIdentity.actorLabels,
|
|
5732
|
+
source: 'node-sdk',
|
|
5733
|
+
},
|
|
5734
|
+
t: requestEpochMs,
|
|
5735
|
+
}],
|
|
5736
|
+
}, { priority: 'high' });
|
|
5737
|
+
}
|
|
5070
5738
|
const resolvePrivacy = (): NormalizedRuntimePrivacyPolicy | null =>
|
|
5071
|
-
getRuntimePrivacyState(
|
|
5739
|
+
getRuntimePrivacyState(captureCfg).policy ?? null;
|
|
5072
5740
|
|
|
5073
|
-
const requestStartRaw = Date.now();
|
|
5074
|
-
const headerTs = readHeaderNumber(req.headers[REQUEST_START_HEADER]) ?? queryTs;
|
|
5075
5741
|
const clockSkewMs = headerTs !== null ? headerTs - requestStartRaw : 0;
|
|
5076
|
-
const requestEpochMs = headerTs !== null ? headerTs : requestStartRaw + clockSkewMs;
|
|
5077
5742
|
const rid = nextRequestId(requestEpochMs);
|
|
5078
5743
|
const captureScope = createRequestCaptureScope(rid, 'http');
|
|
5744
|
+
emitRequestCaptureStart(captureCfg, sid, aid, captureScope, requestEpochMs);
|
|
5079
5745
|
const t0 = requestStartRaw;
|
|
5080
5746
|
const url = (req as any).originalUrl || req.url || '/';
|
|
5081
5747
|
const urlPathOnly = (url || '/').split('?')[0] || '/';
|
|
@@ -5271,7 +5937,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5271
5937
|
?? await materializeInlinePrivacyValueAsync(
|
|
5272
5938
|
'request.body',
|
|
5273
5939
|
sanitizeRequestSnapshot(requestBodyRaw),
|
|
5274
|
-
|
|
5940
|
+
captureCfg,
|
|
5275
5941
|
maskReq,
|
|
5276
5942
|
endpointTraceCtx,
|
|
5277
5943
|
masking,
|
|
@@ -5281,7 +5947,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5281
5947
|
const requestParams = await applyPrivacyThenMaskAsync(
|
|
5282
5948
|
'request.params',
|
|
5283
5949
|
sanitizeRequestSnapshot((req as any).params),
|
|
5284
|
-
|
|
5950
|
+
captureCfg,
|
|
5285
5951
|
maskReq,
|
|
5286
5952
|
endpointTraceCtx,
|
|
5287
5953
|
masking,
|
|
@@ -5290,7 +5956,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5290
5956
|
const requestQuery = await applyPrivacyThenMaskAsync(
|
|
5291
5957
|
'request.query',
|
|
5292
5958
|
sanitizeRequestSnapshot((req as any).query),
|
|
5293
|
-
|
|
5959
|
+
captureCfg,
|
|
5294
5960
|
maskReq,
|
|
5295
5961
|
endpointTraceCtx,
|
|
5296
5962
|
masking,
|
|
@@ -5299,7 +5965,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5299
5965
|
const maskedHeaders = await applyPrivacyThenMaskAsync(
|
|
5300
5966
|
'request.headers',
|
|
5301
5967
|
requestHeaders,
|
|
5302
|
-
|
|
5968
|
+
captureCfg,
|
|
5303
5969
|
maskReq,
|
|
5304
5970
|
endpointTraceCtx,
|
|
5305
5971
|
masking,
|
|
@@ -5311,7 +5977,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5311
5977
|
?? await materializeInlinePrivacyValueAsync(
|
|
5312
5978
|
'response.body',
|
|
5313
5979
|
sanitizeRequestSnapshot(capturedBody),
|
|
5314
|
-
|
|
5980
|
+
captureCfg,
|
|
5315
5981
|
maskReq,
|
|
5316
5982
|
endpointTraceCtx,
|
|
5317
5983
|
masking,
|
|
@@ -5326,7 +5992,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5326
5992
|
rawValue: (req as any).body,
|
|
5327
5993
|
previewValue: requestBody,
|
|
5328
5994
|
capture: {
|
|
5329
|
-
runtimeConfig:
|
|
5995
|
+
runtimeConfig: captureCfg,
|
|
5330
5996
|
captureHeaders: cfg.captureHeaders,
|
|
5331
5997
|
maskReq,
|
|
5332
5998
|
trace: endpointTraceCtx,
|
|
@@ -5339,7 +6005,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5339
6005
|
rawValue: (req as any).params,
|
|
5340
6006
|
previewValue: requestParams,
|
|
5341
6007
|
capture: {
|
|
5342
|
-
runtimeConfig:
|
|
6008
|
+
runtimeConfig: captureCfg,
|
|
5343
6009
|
captureHeaders: cfg.captureHeaders,
|
|
5344
6010
|
maskReq,
|
|
5345
6011
|
trace: endpointTraceCtx,
|
|
@@ -5352,7 +6018,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5352
6018
|
rawValue: (req as any).query,
|
|
5353
6019
|
previewValue: requestQuery,
|
|
5354
6020
|
capture: {
|
|
5355
|
-
runtimeConfig:
|
|
6021
|
+
runtimeConfig: captureCfg,
|
|
5356
6022
|
captureHeaders: cfg.captureHeaders,
|
|
5357
6023
|
maskReq,
|
|
5358
6024
|
trace: endpointTraceCtx,
|
|
@@ -5365,7 +6031,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5365
6031
|
rawValue: req.headers,
|
|
5366
6032
|
previewValue: maskedHeaders,
|
|
5367
6033
|
capture: {
|
|
5368
|
-
runtimeConfig:
|
|
6034
|
+
runtimeConfig: captureCfg,
|
|
5369
6035
|
captureHeaders: cfg.captureHeaders,
|
|
5370
6036
|
maskReq,
|
|
5371
6037
|
trace: endpointTraceCtx,
|
|
@@ -5380,7 +6046,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5380
6046
|
rawValue: capturedBody,
|
|
5381
6047
|
previewValue: responseBody,
|
|
5382
6048
|
capture: {
|
|
5383
|
-
runtimeConfig:
|
|
6049
|
+
runtimeConfig: captureCfg,
|
|
5384
6050
|
captureHeaders: cfg.captureHeaders,
|
|
5385
6051
|
maskReq,
|
|
5386
6052
|
trace: endpointTraceCtx,
|
|
@@ -5440,7 +6106,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5440
6106
|
chosenEndpoint,
|
|
5441
6107
|
hasTraceEvents,
|
|
5442
6108
|
);
|
|
5443
|
-
post(
|
|
6109
|
+
post(captureCfg, sid, {
|
|
5444
6110
|
entries: [{
|
|
5445
6111
|
actionId: aid,
|
|
5446
6112
|
request: requestPayload,
|
|
@@ -5457,7 +6123,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5457
6123
|
const emitRequestCaptureManifest = (traceBatchTotal: number): void => {
|
|
5458
6124
|
if (requestCaptureManifestScheduled) return;
|
|
5459
6125
|
requestCaptureManifestScheduled = true;
|
|
5460
|
-
post(
|
|
6126
|
+
post(captureCfg, sid, {
|
|
5461
6127
|
entries: [{
|
|
5462
6128
|
actionId: aid,
|
|
5463
6129
|
requestRid: rid,
|
|
@@ -5569,7 +6235,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5569
6235
|
try {
|
|
5570
6236
|
const pendingEvents = preparePendingTraceEventsForFlush(events.slice());
|
|
5571
6237
|
const materializedEvents = await materializePendingTraceEventsAsync(pendingEvents, {
|
|
5572
|
-
cfg,
|
|
6238
|
+
cfg: captureCfg,
|
|
5573
6239
|
maskReq,
|
|
5574
6240
|
masking,
|
|
5575
6241
|
resolvePrivacy,
|
|
@@ -5602,7 +6268,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5602
6268
|
const batch = traceBatches[i];
|
|
5603
6269
|
const traceValues = collectBatchTraceValueEntries(batch, i);
|
|
5604
6270
|
|
|
5605
|
-
post(
|
|
6271
|
+
post(captureCfg, sid, {
|
|
5606
6272
|
entries: [{
|
|
5607
6273
|
actionId: aid,
|
|
5608
6274
|
trace: batch,
|
|
@@ -5624,6 +6290,15 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5624
6290
|
|
|
5625
6291
|
sessionDrainWait = waitForSessionDrain(sid);
|
|
5626
6292
|
endSessionRequest(sid);
|
|
6293
|
+
if (backendLiveState && backendLiveChild) {
|
|
6294
|
+
releaseBackendLiveRequest(
|
|
6295
|
+
cfg,
|
|
6296
|
+
normalizeLiveModeConfig(cfg),
|
|
6297
|
+
backendLiveState,
|
|
6298
|
+
backendLiveChild,
|
|
6299
|
+
finishedAtAligned ?? alignedNow(),
|
|
6300
|
+
);
|
|
6301
|
+
}
|
|
5627
6302
|
endTrackedClientRequest();
|
|
5628
6303
|
|
|
5629
6304
|
if (__TRACER_READY) {
|
|
@@ -7165,6 +7840,7 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
|
|
|
7165
7840
|
const span = captureSpanContextFromTracer();
|
|
7166
7841
|
const publishRid = nextRequestId(alignedNow());
|
|
7167
7842
|
const captureScope = createRequestCaptureScope(publishRid, 'kafka');
|
|
7843
|
+
emitRequestCaptureStart(cfg, sid, aid, captureScope);
|
|
7168
7844
|
const payloadObj = payload && typeof payload === 'object' ? payload : {};
|
|
7169
7845
|
const topic = sanitizeKafkaTopic((payloadObj as any).topic);
|
|
7170
7846
|
const rawMessages = Array.isArray((payloadObj as any).messages) ? (payloadObj as any).messages : [];
|
|
@@ -7279,6 +7955,7 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
|
|
|
7279
7955
|
const span = captureSpanContextFromTracer();
|
|
7280
7956
|
const publishRid = nextRequestId(alignedNow());
|
|
7281
7957
|
const captureScope = createRequestCaptureScope(publishRid, 'kafka');
|
|
7958
|
+
emitRequestCaptureStart(cfg, sid, aid, captureScope);
|
|
7282
7959
|
|
|
7283
7960
|
const payloadObj = payload && typeof payload === 'object' ? payload : {};
|
|
7284
7961
|
const topicMessages = Array.isArray((payloadObj as any).topicMessages)
|
|
@@ -7429,6 +8106,7 @@ function wrapKafkaEachMessageHandler(eachMessage: any, cfg: KafkaJsPatchConfig,
|
|
|
7429
8106
|
const consumeRid = nextRequestId(`${topic}-${message?.offset ?? alignedNow()}`);
|
|
7430
8107
|
const traceId = consumeRid;
|
|
7431
8108
|
const captureScope = createRequestCaptureScope(consumeRid, 'kafka');
|
|
8109
|
+
emitRequestCaptureStart(cfg, sid, aid, captureScope);
|
|
7432
8110
|
const rawTraceEvents: any[] = [];
|
|
7433
8111
|
const maskReq: MaskRequestContext = {
|
|
7434
8112
|
method: 'KAFKA_CONSUME',
|
|
@@ -7539,6 +8217,7 @@ function wrapKafkaEachBatchHandler(eachBatch: any, cfg: KafkaJsPatchConfig, cons
|
|
|
7539
8217
|
const consumeRid = nextRequestId(`${topic}-batch-${alignedNow()}`);
|
|
7540
8218
|
const traceId = consumeRid;
|
|
7541
8219
|
const captureScope = createRequestCaptureScope(consumeRid, 'kafka');
|
|
8220
|
+
emitRequestCaptureStart(cfg, sid, aid, captureScope);
|
|
7542
8221
|
const rawTraceEvents: any[] = [];
|
|
7543
8222
|
const maskReq: MaskRequestContext = {
|
|
7544
8223
|
method: 'KAFKA_CONSUME_BATCH',
|
|
@@ -8246,6 +8925,7 @@ function patchAmqplibPublishMethod(
|
|
|
8246
8925
|
const key = `${method} ${methodName === 'publish' ? `${exchange || 'default'}/${routingKey}` : queue}`;
|
|
8247
8926
|
const content = methodName === 'publish' ? args[2] : args[1];
|
|
8248
8927
|
const captureScope = createRequestCaptureScope(publishRid, 'amqp');
|
|
8928
|
+
emitRequestCaptureStart(cfg, sid, aid, captureScope);
|
|
8249
8929
|
const t0 = Date.now();
|
|
8250
8930
|
let finished = false;
|
|
8251
8931
|
|
|
@@ -8335,6 +9015,7 @@ function wrapAmqplibConsumeHandler(onMessage: any, cfg: AmqplibPatchConfig, queu
|
|
|
8335
9015
|
const consumeRid = nextRequestId(`${queueName}-${deliveryTag}`);
|
|
8336
9016
|
const traceId = consumeRid;
|
|
8337
9017
|
const captureScope = createRequestCaptureScope(consumeRid, 'amqp');
|
|
9018
|
+
emitRequestCaptureStart(cfg, sid, aid, captureScope);
|
|
8338
9019
|
const rawTraceEvents: any[] = [];
|
|
8339
9020
|
const maskReq: MaskRequestContext = {
|
|
8340
9021
|
method: 'AMQP_CONSUME',
|
|
@@ -8938,6 +9619,616 @@ export function patchRedis(cfg: RedisPatchConfig) {
|
|
|
8938
9619
|
}
|
|
8939
9620
|
}
|
|
8940
9621
|
|
|
9622
|
+
// ===================================================================
|
|
9623
|
+
// PostgreSQL integrations: pg, postgres.js, and TypeORM
|
|
9624
|
+
// ===================================================================
|
|
9625
|
+
export type PostgreSqlPatchConfig = ReproMiddlewareConfig & {
|
|
9626
|
+
resolveContext?: () => { sid?: string; aid?: string } | undefined;
|
|
9627
|
+
};
|
|
9628
|
+
|
|
9629
|
+
type PostgreSqlSource = {
|
|
9630
|
+
library: 'pg' | 'postgres' | 'typeorm';
|
|
9631
|
+
driver: 'pg' | 'postgres';
|
|
9632
|
+
};
|
|
9633
|
+
|
|
9634
|
+
type PostgreSqlCaptureContext = {
|
|
9635
|
+
cfg: PostgreSqlPatchConfig;
|
|
9636
|
+
sid: string;
|
|
9637
|
+
aid: string | null;
|
|
9638
|
+
spanContext: SpanContext | null;
|
|
9639
|
+
source: PostgreSqlSource;
|
|
9640
|
+
};
|
|
9641
|
+
|
|
9642
|
+
const postgresSqlPatchConfigs: PostgreSqlPatchConfig[] = [];
|
|
9643
|
+
const postgresSqlSourceAls = new AsyncLocalStorage<PostgreSqlSource>();
|
|
9644
|
+
const pgPoolQueryAls = new AsyncLocalStorage<boolean>();
|
|
9645
|
+
let pgRequireHookInstalled = false;
|
|
9646
|
+
let postgresJsRequireHookInstalled = false;
|
|
9647
|
+
let typeOrmRequireHookInstalled = false;
|
|
9648
|
+
|
|
9649
|
+
const PG_CLIENT_PATCHED = Symbol.for('@reproapp/node-sdk/pg-client-patched');
|
|
9650
|
+
const PG_POOL_PATCHED = Symbol.for('@reproapp/node-sdk/pg-pool-patched');
|
|
9651
|
+
const POSTGRES_JS_QUERY_PATCHED = Symbol.for('@reproapp/node-sdk/postgres-js-query-patched');
|
|
9652
|
+
const TYPEORM_DATASOURCE_PATCHED = Symbol.for('@reproapp/node-sdk/typeorm-datasource-patched');
|
|
9653
|
+
const TYPEORM_QUERY_RUNNER_PATCHED = Symbol.for('@reproapp/node-sdk/typeorm-query-runner-patched');
|
|
9654
|
+
|
|
9655
|
+
function registerPostgreSqlPatchConfig(cfg: PostgreSqlPatchConfig): void {
|
|
9656
|
+
if (!postgresSqlPatchConfigs.includes(cfg)) {
|
|
9657
|
+
postgresSqlPatchConfigs.push(cfg);
|
|
9658
|
+
}
|
|
9659
|
+
}
|
|
9660
|
+
|
|
9661
|
+
function resolvePostgreSqlCaptureContext(defaultSource: PostgreSqlSource, source?: any): PostgreSqlCaptureContext | null {
|
|
9662
|
+
const ctx = getCtx();
|
|
9663
|
+
const contextualSource = postgresSqlSourceAls.getStore();
|
|
9664
|
+
for (let i = postgresSqlPatchConfigs.length - 1; i >= 0; i -= 1) {
|
|
9665
|
+
const cfg = postgresSqlPatchConfigs[i];
|
|
9666
|
+
if (!cfg) continue;
|
|
9667
|
+
const fallback = cfg.resolveContext?.() ?? {};
|
|
9668
|
+
const sid = ctx.sid ?? fallback.sid ?? null;
|
|
9669
|
+
if (!sid) continue;
|
|
9670
|
+
return {
|
|
9671
|
+
cfg,
|
|
9672
|
+
sid,
|
|
9673
|
+
aid: ctx.aid ?? fallback.aid ?? null,
|
|
9674
|
+
spanContext: captureSpanContextFromTracer(source),
|
|
9675
|
+
source: contextualSource ?? defaultSource,
|
|
9676
|
+
};
|
|
9677
|
+
}
|
|
9678
|
+
return null;
|
|
9679
|
+
}
|
|
9680
|
+
|
|
9681
|
+
function stripSqlComments(sql: string): string {
|
|
9682
|
+
let remaining = sql.trimStart();
|
|
9683
|
+
while (remaining.length) {
|
|
9684
|
+
if (remaining.startsWith('--')) {
|
|
9685
|
+
const newline = remaining.indexOf('\n');
|
|
9686
|
+
remaining = newline < 0 ? '' : remaining.slice(newline + 1).trimStart();
|
|
9687
|
+
continue;
|
|
9688
|
+
}
|
|
9689
|
+
if (remaining.startsWith('/*')) {
|
|
9690
|
+
const end = remaining.indexOf('*/', 2);
|
|
9691
|
+
remaining = end < 0 ? '' : remaining.slice(end + 2).trimStart();
|
|
9692
|
+
continue;
|
|
9693
|
+
}
|
|
9694
|
+
break;
|
|
9695
|
+
}
|
|
9696
|
+
return remaining;
|
|
9697
|
+
}
|
|
9698
|
+
|
|
9699
|
+
function normalizeSqlOperation(sql: string): string {
|
|
9700
|
+
const normalized = stripSqlComments(sql);
|
|
9701
|
+
const first = normalized.match(/^([a-z]+)/i)?.[1]?.toLowerCase() ?? 'query';
|
|
9702
|
+
if (first !== 'with') return first;
|
|
9703
|
+
|
|
9704
|
+
// CTEs can wrap any DML statement. The last top-level DML keyword is the
|
|
9705
|
+
// operation users care about, while nested SELECTs only build the CTE.
|
|
9706
|
+
const matches = Array.from(normalized.matchAll(/\b(select|insert|update|delete|merge)\b/gi));
|
|
9707
|
+
return matches.length ? String(matches[matches.length - 1][1]).toLowerCase() : 'query';
|
|
9708
|
+
}
|
|
9709
|
+
|
|
9710
|
+
function cleanSqlIdentifier(value: string | undefined): string | undefined {
|
|
9711
|
+
if (!value) return undefined;
|
|
9712
|
+
const cleaned = value
|
|
9713
|
+
.trim()
|
|
9714
|
+
.replace(/[;,]$/, '')
|
|
9715
|
+
.split('.')
|
|
9716
|
+
.map((part) => part.replace(/^['"`]|['"`]$/g, ''))
|
|
9717
|
+
.join('.');
|
|
9718
|
+
return cleaned || undefined;
|
|
9719
|
+
}
|
|
9720
|
+
|
|
9721
|
+
function resolveSqlTable(sql: string, op: string): string {
|
|
9722
|
+
const normalized = stripSqlComments(sql);
|
|
9723
|
+
let match: RegExpMatchArray | null = null;
|
|
9724
|
+
if (op === 'insert') match = normalized.match(/\binsert\s+into\s+([^\s(]+)/i);
|
|
9725
|
+
else if (op === 'update') match = normalized.match(/\bupdate\s+([^\s(]+)/i);
|
|
9726
|
+
else if (op === 'delete') match = normalized.match(/\bdelete\s+from\s+([^\s(]+)/i);
|
|
9727
|
+
else if (op === 'merge') match = normalized.match(/\bmerge\s+into\s+([^\s(]+)/i);
|
|
9728
|
+
else match = normalized.match(/\bfrom\s+([^\s(]+)/i);
|
|
9729
|
+
return cleanSqlIdentifier(match?.[1]) ?? 'unknown';
|
|
9730
|
+
}
|
|
9731
|
+
|
|
9732
|
+
function buildPostgresQueryPayload(
|
|
9733
|
+
sql: string,
|
|
9734
|
+
parameters: any[],
|
|
9735
|
+
source: PostgreSqlSource,
|
|
9736
|
+
extra?: Record<string, any>,
|
|
9737
|
+
): Record<string, any> {
|
|
9738
|
+
return {
|
|
9739
|
+
sql,
|
|
9740
|
+
parameters,
|
|
9741
|
+
library: source.library,
|
|
9742
|
+
driver: source.driver,
|
|
9743
|
+
...(extra ?? {}),
|
|
9744
|
+
};
|
|
9745
|
+
}
|
|
9746
|
+
|
|
9747
|
+
function summarizePostgresError(error: any): Record<string, any> {
|
|
9748
|
+
return {
|
|
9749
|
+
name: error?.name ?? 'Error',
|
|
9750
|
+
message: error?.message ?? String(error),
|
|
9751
|
+
code: error?.code,
|
|
9752
|
+
severity: error?.severity,
|
|
9753
|
+
detail: error?.detail,
|
|
9754
|
+
hint: error?.hint,
|
|
9755
|
+
position: error?.position,
|
|
9756
|
+
internalPosition: error?.internalPosition,
|
|
9757
|
+
internalQuery: error?.internalQuery,
|
|
9758
|
+
where: error?.where,
|
|
9759
|
+
schema: error?.schema,
|
|
9760
|
+
table: error?.table,
|
|
9761
|
+
column: error?.column,
|
|
9762
|
+
dataType: error?.dataType,
|
|
9763
|
+
constraint: error?.constraint,
|
|
9764
|
+
file: error?.file,
|
|
9765
|
+
line: error?.line,
|
|
9766
|
+
routine: error?.routine,
|
|
9767
|
+
};
|
|
9768
|
+
}
|
|
9769
|
+
|
|
9770
|
+
function normalizePgQueryInput(config: any, values: any): { sql: string; parameters: any[]; extra?: Record<string, any> } {
|
|
9771
|
+
if (typeof config === 'string') {
|
|
9772
|
+
return { sql: config, parameters: Array.isArray(values) ? values : [] };
|
|
9773
|
+
}
|
|
9774
|
+
const sql = typeof config?.text === 'string' ? config.text : '';
|
|
9775
|
+
const parameters = Array.isArray(config?.values)
|
|
9776
|
+
? config.values
|
|
9777
|
+
: Array.isArray(values)
|
|
9778
|
+
? values
|
|
9779
|
+
: [];
|
|
9780
|
+
return {
|
|
9781
|
+
sql,
|
|
9782
|
+
parameters,
|
|
9783
|
+
extra: {
|
|
9784
|
+
name: typeof config?.name === 'string' ? config.name : undefined,
|
|
9785
|
+
rowMode: config?.rowMode,
|
|
9786
|
+
binary: config?.binary,
|
|
9787
|
+
},
|
|
9788
|
+
};
|
|
9789
|
+
}
|
|
9790
|
+
|
|
9791
|
+
function normalizePgResult(result: any): any {
|
|
9792
|
+
if (Array.isArray(result)) return result.map(normalizePgResult);
|
|
9793
|
+
if (!result || typeof result !== 'object') return result;
|
|
9794
|
+
return {
|
|
9795
|
+
command: result.command,
|
|
9796
|
+
rowCount: result.rowCount,
|
|
9797
|
+
oid: result.oid,
|
|
9798
|
+
fields: Array.isArray(result.fields)
|
|
9799
|
+
? result.fields.map((field: any) => ({
|
|
9800
|
+
name: field?.name,
|
|
9801
|
+
tableID: field?.tableID,
|
|
9802
|
+
columnID: field?.columnID,
|
|
9803
|
+
dataTypeID: field?.dataTypeID,
|
|
9804
|
+
dataTypeSize: field?.dataTypeSize,
|
|
9805
|
+
dataTypeModifier: field?.dataTypeModifier,
|
|
9806
|
+
format: field?.format,
|
|
9807
|
+
}))
|
|
9808
|
+
: undefined,
|
|
9809
|
+
rows: result.rows,
|
|
9810
|
+
};
|
|
9811
|
+
}
|
|
9812
|
+
|
|
9813
|
+
function emitPostgreSqlResult(
|
|
9814
|
+
capture: PostgreSqlCaptureContext,
|
|
9815
|
+
sql: string,
|
|
9816
|
+
parameters: any[],
|
|
9817
|
+
startedAt: number,
|
|
9818
|
+
result: any,
|
|
9819
|
+
error?: any,
|
|
9820
|
+
extra?: Record<string, any>,
|
|
9821
|
+
): void {
|
|
9822
|
+
const op = normalizeSqlOperation(sql);
|
|
9823
|
+
emitDbQuery(capture.cfg, capture.sid, capture.aid ?? undefined, {
|
|
9824
|
+
type: 'postgresql',
|
|
9825
|
+
collection: resolveSqlTable(sql, op),
|
|
9826
|
+
op,
|
|
9827
|
+
queryRaw: buildPostgresQueryPayload(sql, parameters, capture.source, extra),
|
|
9828
|
+
resultMetaRaw: error ? undefined : result,
|
|
9829
|
+
error: error ? summarizePostgresError(error) : undefined,
|
|
9830
|
+
durMs: Date.now() - startedAt,
|
|
9831
|
+
t: alignedNow(),
|
|
9832
|
+
spanContext: capture.spanContext,
|
|
9833
|
+
});
|
|
9834
|
+
}
|
|
9835
|
+
|
|
9836
|
+
function findPgCallbackIndex(args: any[]): number {
|
|
9837
|
+
for (let i = args.length - 1; i >= 1; i -= 1) {
|
|
9838
|
+
if (typeof args[i] === 'function') return i;
|
|
9839
|
+
}
|
|
9840
|
+
return -1;
|
|
9841
|
+
}
|
|
9842
|
+
|
|
9843
|
+
function patchPgClientPrototype(proto: any): void {
|
|
9844
|
+
if (!proto || proto[PG_CLIENT_PATCHED]) return;
|
|
9845
|
+
const original = proto.query;
|
|
9846
|
+
if (typeof original !== 'function') return;
|
|
9847
|
+
try {
|
|
9848
|
+
Object.defineProperty(proto, PG_CLIENT_PATCHED, { value: true, configurable: true });
|
|
9849
|
+
} catch {
|
|
9850
|
+
return;
|
|
9851
|
+
}
|
|
9852
|
+
|
|
9853
|
+
proto.query = function reproPatchedPgQuery(this: any, ...args: any[]) {
|
|
9854
|
+
if (pgPoolQueryAls.getStore()) return original.apply(this, args);
|
|
9855
|
+
const capture = resolvePostgreSqlCaptureContext({ library: 'pg', driver: 'pg' }, this);
|
|
9856
|
+
if (!capture) return original.apply(this, args);
|
|
9857
|
+
|
|
9858
|
+
const input = normalizePgQueryInput(args[0], args[1]);
|
|
9859
|
+
const startedAt = Date.now();
|
|
9860
|
+
const callbackIndex = findPgCallbackIndex(args);
|
|
9861
|
+
const actualArgs = callbackIndex >= 0 ? [...args] : args;
|
|
9862
|
+
let emitted = false;
|
|
9863
|
+
const emitSuccess = (result: any) => {
|
|
9864
|
+
if (emitted) return;
|
|
9865
|
+
emitted = true;
|
|
9866
|
+
emitPostgreSqlResult(
|
|
9867
|
+
capture,
|
|
9868
|
+
input.sql,
|
|
9869
|
+
input.parameters,
|
|
9870
|
+
startedAt,
|
|
9871
|
+
normalizePgResult(result),
|
|
9872
|
+
undefined,
|
|
9873
|
+
input.extra,
|
|
9874
|
+
);
|
|
9875
|
+
};
|
|
9876
|
+
const emitFailure = (error: any) => {
|
|
9877
|
+
if (emitted) return;
|
|
9878
|
+
emitted = true;
|
|
9879
|
+
emitPostgreSqlResult(capture, input.sql, input.parameters, startedAt, undefined, error, input.extra);
|
|
9880
|
+
};
|
|
9881
|
+
|
|
9882
|
+
if (callbackIndex >= 0) {
|
|
9883
|
+
const callback = args[callbackIndex];
|
|
9884
|
+
actualArgs[callbackIndex] = function reproPgQueryCallback(this: any, error: any, result: any, ...rest: any[]) {
|
|
9885
|
+
if (error) emitFailure(error);
|
|
9886
|
+
else emitSuccess(result);
|
|
9887
|
+
return callback.call(this, error, result, ...rest);
|
|
9888
|
+
};
|
|
9889
|
+
}
|
|
9890
|
+
|
|
9891
|
+
try {
|
|
9892
|
+
const returned = original.apply(this, actualArgs);
|
|
9893
|
+
if (returned && typeof returned.then === 'function') {
|
|
9894
|
+
return returned.then(
|
|
9895
|
+
(result: any) => {
|
|
9896
|
+
emitSuccess(result);
|
|
9897
|
+
return result;
|
|
9898
|
+
},
|
|
9899
|
+
(error: any) => {
|
|
9900
|
+
emitFailure(error);
|
|
9901
|
+
throw error;
|
|
9902
|
+
},
|
|
9903
|
+
);
|
|
9904
|
+
}
|
|
9905
|
+
if (returned && callbackIndex < 0 && typeof returned.once === 'function') {
|
|
9906
|
+
returned.once('end', emitSuccess);
|
|
9907
|
+
returned.once('error', emitFailure);
|
|
9908
|
+
}
|
|
9909
|
+
return returned;
|
|
9910
|
+
} catch (error) {
|
|
9911
|
+
emitFailure(error);
|
|
9912
|
+
throw error;
|
|
9913
|
+
}
|
|
9914
|
+
};
|
|
9915
|
+
}
|
|
9916
|
+
|
|
9917
|
+
function patchPgPoolPrototype(proto: any): void {
|
|
9918
|
+
if (!proto || proto[PG_POOL_PATCHED]) return;
|
|
9919
|
+
const original = proto.query;
|
|
9920
|
+
if (typeof original !== 'function') return;
|
|
9921
|
+
try {
|
|
9922
|
+
Object.defineProperty(proto, PG_POOL_PATCHED, { value: true, configurable: true });
|
|
9923
|
+
} catch {
|
|
9924
|
+
return;
|
|
9925
|
+
}
|
|
9926
|
+
|
|
9927
|
+
proto.query = function reproPatchedPgPoolQuery(this: any, ...args: any[]) {
|
|
9928
|
+
const capture = resolvePostgreSqlCaptureContext({ library: 'pg', driver: 'pg' }, this);
|
|
9929
|
+
if (!capture) return original.apply(this, args);
|
|
9930
|
+
|
|
9931
|
+
const input = normalizePgQueryInput(args[0], args[1]);
|
|
9932
|
+
const startedAt = Date.now();
|
|
9933
|
+
const callbackIndex = findPgCallbackIndex(args);
|
|
9934
|
+
const actualArgs = callbackIndex >= 0 ? [...args] : args;
|
|
9935
|
+
let emitted = false;
|
|
9936
|
+
const emitSuccess = (result: any) => {
|
|
9937
|
+
if (emitted) return;
|
|
9938
|
+
emitted = true;
|
|
9939
|
+
emitPostgreSqlResult(
|
|
9940
|
+
capture,
|
|
9941
|
+
input.sql,
|
|
9942
|
+
input.parameters,
|
|
9943
|
+
startedAt,
|
|
9944
|
+
normalizePgResult(result),
|
|
9945
|
+
undefined,
|
|
9946
|
+
input.extra,
|
|
9947
|
+
);
|
|
9948
|
+
};
|
|
9949
|
+
const emitFailure = (error: any) => {
|
|
9950
|
+
if (emitted) return;
|
|
9951
|
+
emitted = true;
|
|
9952
|
+
emitPostgreSqlResult(capture, input.sql, input.parameters, startedAt, undefined, error, input.extra);
|
|
9953
|
+
};
|
|
9954
|
+
|
|
9955
|
+
if (callbackIndex >= 0) {
|
|
9956
|
+
const callback = args[callbackIndex];
|
|
9957
|
+
actualArgs[callbackIndex] = function reproPgPoolQueryCallback(this: any, error: any, result: any, ...rest: any[]) {
|
|
9958
|
+
if (error) emitFailure(error);
|
|
9959
|
+
else emitSuccess(result);
|
|
9960
|
+
return callback.call(this, error, result, ...rest);
|
|
9961
|
+
};
|
|
9962
|
+
}
|
|
9963
|
+
|
|
9964
|
+
try {
|
|
9965
|
+
const returned = pgPoolQueryAls.run(true, () => original.apply(this, actualArgs));
|
|
9966
|
+
if (returned && typeof returned.then === 'function') {
|
|
9967
|
+
return returned.then(
|
|
9968
|
+
(result: any) => {
|
|
9969
|
+
emitSuccess(result);
|
|
9970
|
+
return result;
|
|
9971
|
+
},
|
|
9972
|
+
(error: any) => {
|
|
9973
|
+
emitFailure(error);
|
|
9974
|
+
throw error;
|
|
9975
|
+
},
|
|
9976
|
+
);
|
|
9977
|
+
}
|
|
9978
|
+
return returned;
|
|
9979
|
+
} catch (error) {
|
|
9980
|
+
emitFailure(error);
|
|
9981
|
+
throw error;
|
|
9982
|
+
}
|
|
9983
|
+
};
|
|
9984
|
+
}
|
|
9985
|
+
|
|
9986
|
+
function patchPgFromModuleExports(pgModule: any): void {
|
|
9987
|
+
if (!pgModule) return;
|
|
9988
|
+
const candidates = [pgModule, pgModule.default];
|
|
9989
|
+
for (const candidate of candidates) {
|
|
9990
|
+
patchPgClientPrototype(candidate?.Client?.prototype);
|
|
9991
|
+
patchPgClientPrototype(candidate?.native?.Client?.prototype);
|
|
9992
|
+
patchPgPoolPrototype(candidate?.Pool?.prototype);
|
|
9993
|
+
patchPgPoolPrototype(candidate?.native?.Pool?.prototype);
|
|
9994
|
+
}
|
|
9995
|
+
}
|
|
9996
|
+
|
|
9997
|
+
function installPgRequireHook(): void {
|
|
9998
|
+
if (pgRequireHookInstalled) return;
|
|
9999
|
+
pgRequireHookInstalled = true;
|
|
10000
|
+
try {
|
|
10001
|
+
const rim = requireOptionalHostModule('require-in-the-middle');
|
|
10002
|
+
const Hook = rim?.Hook ?? rim;
|
|
10003
|
+
if (typeof Hook !== 'function') return;
|
|
10004
|
+
// eslint-disable-next-line no-new
|
|
10005
|
+
new Hook(['pg'], (exports: any) => {
|
|
10006
|
+
patchPgFromModuleExports(exports);
|
|
10007
|
+
return exports;
|
|
10008
|
+
});
|
|
10009
|
+
} catch {}
|
|
10010
|
+
}
|
|
10011
|
+
|
|
10012
|
+
function normalizePostgresJsInput(query: any): { sql: string; parameters: any[]; extra: Record<string, any> } {
|
|
10013
|
+
if (typeof query?.statement?.string === 'string') {
|
|
10014
|
+
return {
|
|
10015
|
+
sql: query.statement.string,
|
|
10016
|
+
parameters: Array.isArray(query?.parameters) ? query.parameters : [],
|
|
10017
|
+
extra: {
|
|
10018
|
+
tagged: query?.tagged === true,
|
|
10019
|
+
parameterTypes: Array.isArray(query?.statement?.types) ? query.statement.types : undefined,
|
|
10020
|
+
},
|
|
10021
|
+
};
|
|
10022
|
+
}
|
|
10023
|
+
const strings = Array.isArray(query?.strings) ? query.strings : [];
|
|
10024
|
+
const parameters = Array.isArray(query?.args) ? query.args : [];
|
|
10025
|
+
if (query?.tagged) {
|
|
10026
|
+
return {
|
|
10027
|
+
sql: strings.map((part: any, index: number) => `${String(part)}${index < parameters.length ? `$${index + 1}` : ''}`).join(''),
|
|
10028
|
+
parameters,
|
|
10029
|
+
extra: { tagged: true },
|
|
10030
|
+
};
|
|
10031
|
+
}
|
|
10032
|
+
return {
|
|
10033
|
+
sql: typeof strings[0] === 'string' ? strings[0] : '',
|
|
10034
|
+
parameters,
|
|
10035
|
+
extra: { tagged: false },
|
|
10036
|
+
};
|
|
10037
|
+
}
|
|
10038
|
+
|
|
10039
|
+
function normalizePostgresJsResult(result: any): any {
|
|
10040
|
+
if (!Array.isArray(result)) return result;
|
|
10041
|
+
const metadata = result as any;
|
|
10042
|
+
return {
|
|
10043
|
+
command: metadata.command,
|
|
10044
|
+
count: metadata.count,
|
|
10045
|
+
columns: Array.isArray(metadata.columns)
|
|
10046
|
+
? metadata.columns.map((column: any) => ({
|
|
10047
|
+
name: column?.name,
|
|
10048
|
+
type: column?.type,
|
|
10049
|
+
table: column?.table,
|
|
10050
|
+
number: column?.number,
|
|
10051
|
+
}))
|
|
10052
|
+
: undefined,
|
|
10053
|
+
rows: Array.from(result),
|
|
10054
|
+
};
|
|
10055
|
+
}
|
|
10056
|
+
|
|
10057
|
+
function patchPostgresJsQueryPrototype(proto: any): void {
|
|
10058
|
+
if (!proto || proto[POSTGRES_JS_QUERY_PATCHED]) return;
|
|
10059
|
+
const originalHandle = proto.handle;
|
|
10060
|
+
if (typeof originalHandle !== 'function') return;
|
|
10061
|
+
try {
|
|
10062
|
+
Object.defineProperty(proto, POSTGRES_JS_QUERY_PATCHED, { value: true, configurable: true });
|
|
10063
|
+
} catch {
|
|
10064
|
+
return;
|
|
10065
|
+
}
|
|
10066
|
+
|
|
10067
|
+
proto.handle = function reproPatchedPostgresJsHandle(this: any, ...args: any[]) {
|
|
10068
|
+
if (!this.__repro_postgres_capture_attached) {
|
|
10069
|
+
const capture = resolvePostgreSqlCaptureContext({ library: 'postgres', driver: 'postgres' }, this);
|
|
10070
|
+
if (capture) {
|
|
10071
|
+
try {
|
|
10072
|
+
Object.defineProperty(this, '__repro_postgres_capture_attached', { value: true, configurable: true });
|
|
10073
|
+
} catch {
|
|
10074
|
+
this.__repro_postgres_capture_attached = true;
|
|
10075
|
+
}
|
|
10076
|
+
const input = normalizePostgresJsInput(this);
|
|
10077
|
+
const startedAt = Date.now();
|
|
10078
|
+
Promise.prototype.then.call(
|
|
10079
|
+
this,
|
|
10080
|
+
(result: any) => {
|
|
10081
|
+
const executedInput = normalizePostgresJsInput(this);
|
|
10082
|
+
emitPostgreSqlResult(
|
|
10083
|
+
capture,
|
|
10084
|
+
executedInput.sql || input.sql,
|
|
10085
|
+
executedInput.parameters,
|
|
10086
|
+
startedAt,
|
|
10087
|
+
normalizePostgresJsResult(result),
|
|
10088
|
+
undefined,
|
|
10089
|
+
executedInput.extra,
|
|
10090
|
+
);
|
|
10091
|
+
return result;
|
|
10092
|
+
},
|
|
10093
|
+
(error: any) => {
|
|
10094
|
+
const executedInput = normalizePostgresJsInput(this);
|
|
10095
|
+
emitPostgreSqlResult(
|
|
10096
|
+
capture,
|
|
10097
|
+
executedInput.sql || input.sql,
|
|
10098
|
+
executedInput.parameters.length ? executedInput.parameters : input.parameters,
|
|
10099
|
+
startedAt,
|
|
10100
|
+
undefined,
|
|
10101
|
+
error,
|
|
10102
|
+
executedInput.extra,
|
|
10103
|
+
);
|
|
10104
|
+
},
|
|
10105
|
+
);
|
|
10106
|
+
}
|
|
10107
|
+
}
|
|
10108
|
+
return originalHandle.apply(this, args);
|
|
10109
|
+
};
|
|
10110
|
+
}
|
|
10111
|
+
|
|
10112
|
+
function patchPostgresJsFromModuleExports(postgresModule: any): void {
|
|
10113
|
+
const factories = new Set<any>();
|
|
10114
|
+
const addFactory = (candidate: any) => {
|
|
10115
|
+
if (typeof candidate !== 'function') return;
|
|
10116
|
+
// getLoadedModuleExportsByPackage also returns internal postgres.js
|
|
10117
|
+
// helpers. Only the public factory carries these static exports.
|
|
10118
|
+
if (!candidate.PostgresError && !candidate.BigInt && !candidate.__repro_postgres_factory) return;
|
|
10119
|
+
factories.add(candidate);
|
|
10120
|
+
};
|
|
10121
|
+
addFactory(postgresModule);
|
|
10122
|
+
addFactory(postgresModule?.default);
|
|
10123
|
+
|
|
10124
|
+
for (const factory of factories) {
|
|
10125
|
+
let sql: any;
|
|
10126
|
+
try {
|
|
10127
|
+
sql = factory({ max: 1, idle_timeout: 1, connect_timeout: 1 });
|
|
10128
|
+
const query = sql.unsafe('select 1');
|
|
10129
|
+
patchPostgresJsQueryPrototype(Object.getPrototypeOf(query));
|
|
10130
|
+
} catch {}
|
|
10131
|
+
try {
|
|
10132
|
+
const ended = sql?.end?.({ timeout: 0 });
|
|
10133
|
+
if (ended && typeof ended.catch === 'function') ended.catch(() => undefined);
|
|
10134
|
+
} catch {}
|
|
10135
|
+
}
|
|
10136
|
+
}
|
|
10137
|
+
|
|
10138
|
+
function installPostgresJsRequireHook(): void {
|
|
10139
|
+
if (postgresJsRequireHookInstalled) return;
|
|
10140
|
+
postgresJsRequireHookInstalled = true;
|
|
10141
|
+
try {
|
|
10142
|
+
const rim = requireOptionalHostModule('require-in-the-middle');
|
|
10143
|
+
const Hook = rim?.Hook ?? rim;
|
|
10144
|
+
if (typeof Hook !== 'function') return;
|
|
10145
|
+
// eslint-disable-next-line no-new
|
|
10146
|
+
new Hook(['postgres'], (exports: any) => {
|
|
10147
|
+
patchPostgresJsFromModuleExports(exports);
|
|
10148
|
+
return exports;
|
|
10149
|
+
});
|
|
10150
|
+
} catch {}
|
|
10151
|
+
}
|
|
10152
|
+
|
|
10153
|
+
function patchTypeOrmQueryRunner(queryRunner: any): any {
|
|
10154
|
+
if (!queryRunner || queryRunner[TYPEORM_QUERY_RUNNER_PATCHED]) return queryRunner;
|
|
10155
|
+
const original = queryRunner.query;
|
|
10156
|
+
if (typeof original !== 'function') return queryRunner;
|
|
10157
|
+
try {
|
|
10158
|
+
Object.defineProperty(queryRunner, TYPEORM_QUERY_RUNNER_PATCHED, { value: true, configurable: true });
|
|
10159
|
+
} catch {
|
|
10160
|
+
return queryRunner;
|
|
10161
|
+
}
|
|
10162
|
+
queryRunner.query = function reproPatchedTypeOrmQuery(this: any, ...args: any[]) {
|
|
10163
|
+
return postgresSqlSourceAls.run(
|
|
10164
|
+
{ library: 'typeorm', driver: 'pg' },
|
|
10165
|
+
() => original.apply(this, args),
|
|
10166
|
+
);
|
|
10167
|
+
};
|
|
10168
|
+
return queryRunner;
|
|
10169
|
+
}
|
|
10170
|
+
|
|
10171
|
+
function patchTypeOrmDataSourcePrototype(proto: any): void {
|
|
10172
|
+
if (!proto || proto[TYPEORM_DATASOURCE_PATCHED]) return;
|
|
10173
|
+
const original = proto.createQueryRunner;
|
|
10174
|
+
if (typeof original !== 'function') return;
|
|
10175
|
+
try {
|
|
10176
|
+
Object.defineProperty(proto, TYPEORM_DATASOURCE_PATCHED, { value: true, configurable: true });
|
|
10177
|
+
} catch {
|
|
10178
|
+
return;
|
|
10179
|
+
}
|
|
10180
|
+
proto.createQueryRunner = function reproPatchedCreateQueryRunner(this: any, ...args: any[]) {
|
|
10181
|
+
return patchTypeOrmQueryRunner(original.apply(this, args));
|
|
10182
|
+
};
|
|
10183
|
+
}
|
|
10184
|
+
|
|
10185
|
+
function patchTypeOrmFromModuleExports(typeOrmModule: any): void {
|
|
10186
|
+
if (!typeOrmModule) return;
|
|
10187
|
+
patchTypeOrmDataSourcePrototype(typeOrmModule?.DataSource?.prototype);
|
|
10188
|
+
patchTypeOrmDataSourcePrototype(typeOrmModule?.Connection?.prototype);
|
|
10189
|
+
patchTypeOrmDataSourcePrototype(typeOrmModule?.default?.DataSource?.prototype);
|
|
10190
|
+
}
|
|
10191
|
+
|
|
10192
|
+
function installTypeOrmRequireHook(): void {
|
|
10193
|
+
if (typeOrmRequireHookInstalled) return;
|
|
10194
|
+
typeOrmRequireHookInstalled = true;
|
|
10195
|
+
try {
|
|
10196
|
+
const rim = requireOptionalHostModule('require-in-the-middle');
|
|
10197
|
+
const Hook = rim?.Hook ?? rim;
|
|
10198
|
+
if (typeof Hook !== 'function') return;
|
|
10199
|
+
// eslint-disable-next-line no-new
|
|
10200
|
+
new Hook(['typeorm'], (exports: any) => {
|
|
10201
|
+
patchTypeOrmFromModuleExports(exports);
|
|
10202
|
+
return exports;
|
|
10203
|
+
});
|
|
10204
|
+
} catch {}
|
|
10205
|
+
}
|
|
10206
|
+
|
|
10207
|
+
export function patchPostgreSql(cfg: PostgreSqlPatchConfig): void {
|
|
10208
|
+
registerPostgreSqlPatchConfig(cfg);
|
|
10209
|
+
installPgRequireHook();
|
|
10210
|
+
installPostgresJsRequireHook();
|
|
10211
|
+
installTypeOrmRequireHook();
|
|
10212
|
+
|
|
10213
|
+
const pgModules = new Set<any>();
|
|
10214
|
+
const pg = requireOptionalHostModule('pg');
|
|
10215
|
+
if (pg) pgModules.add(pg);
|
|
10216
|
+
for (const loaded of getLoadedModuleExportsByPackage('pg')) pgModules.add(loaded);
|
|
10217
|
+
for (const loaded of pgModules) patchPgFromModuleExports(loaded);
|
|
10218
|
+
|
|
10219
|
+
const postgresModules = new Set<any>();
|
|
10220
|
+
const postgres = requireOptionalHostModule('postgres');
|
|
10221
|
+
if (postgres) postgresModules.add(postgres);
|
|
10222
|
+
for (const loaded of getLoadedModuleExportsByPackage('postgres')) postgresModules.add(loaded);
|
|
10223
|
+
for (const loaded of postgresModules) patchPostgresJsFromModuleExports(loaded);
|
|
10224
|
+
|
|
10225
|
+
const typeOrmModules = new Set<any>();
|
|
10226
|
+
const typeOrm = requireOptionalHostModule('typeorm');
|
|
10227
|
+
if (typeOrm) typeOrmModules.add(typeOrm);
|
|
10228
|
+
for (const loaded of getLoadedModuleExportsByPackage('typeorm')) typeOrmModules.add(loaded);
|
|
10229
|
+
for (const loaded of typeOrmModules) patchTypeOrmFromModuleExports(loaded);
|
|
10230
|
+
}
|
|
10231
|
+
|
|
8941
10232
|
// ===================================================================
|
|
8942
10233
|
// Native MongoDB driver integration
|
|
8943
10234
|
// ===================================================================
|
|
@@ -10045,9 +11336,11 @@ export async function initRepro(cfg: ReproInitConfig): Promise<void> {
|
|
|
10045
11336
|
const ingestCfg = toIngestConfig(cfg);
|
|
10046
11337
|
reproAutoMiddlewareConfig = {
|
|
10047
11338
|
...ingestCfg,
|
|
11339
|
+
apiBase: cfg.apiBase,
|
|
10048
11340
|
captureHeaders: cfg.captureHeaders,
|
|
10049
11341
|
masking: cfg.masking,
|
|
10050
11342
|
privacy: cfg.privacy,
|
|
11343
|
+
liveMode: cfg.liveMode,
|
|
10051
11344
|
};
|
|
10052
11345
|
const integrationCfg = {
|
|
10053
11346
|
...reproAutoMiddlewareConfig,
|
|
@@ -10066,6 +11359,7 @@ export async function initRepro(cfg: ReproInitConfig): Promise<void> {
|
|
|
10066
11359
|
patchKafkaJs(integrationCfg);
|
|
10067
11360
|
patchAmqplib(integrationCfg);
|
|
10068
11361
|
patchRedis(integrationCfg);
|
|
11362
|
+
patchPostgreSql(integrationCfg);
|
|
10069
11363
|
patchMongoDb(integrationCfg);
|
|
10070
11364
|
await installMongoDbImportHook();
|
|
10071
11365
|
patchSendgridMail(integrationCfg);
|
|
@@ -10102,6 +11396,9 @@ export const __reproTestHooks = {
|
|
|
10102
11396
|
lastClientActivityAt = 0;
|
|
10103
11397
|
sdkBackgroundQuietUntil = 0;
|
|
10104
11398
|
},
|
|
11399
|
+
setBackendLiveTimingForTest(override: Partial<BackendLiveTimingConfig> | null): void {
|
|
11400
|
+
backendLiveTimingOverrideForTest = override;
|
|
11401
|
+
},
|
|
10105
11402
|
runWithRequestContextForTest<T>(ctx: Partial<Ctx>, fn: () => T): T {
|
|
10106
11403
|
return als.run({
|
|
10107
11404
|
...ctx,
|
|
@@ -10129,6 +11426,9 @@ export const __reproTestHooks = {
|
|
|
10129
11426
|
patchAmqplibConnectionForTest: patchAmqplibConnection,
|
|
10130
11427
|
patchAmqplibChannelForTest: patchAmqplibChannel,
|
|
10131
11428
|
wrapAmqplibConsumeHandlerForTest: wrapAmqplibConsumeHandler,
|
|
11429
|
+
patchPgModuleForTest: patchPgFromModuleExports,
|
|
11430
|
+
patchPostgresJsModuleForTest: patchPostgresJsFromModuleExports,
|
|
11431
|
+
patchTypeOrmModuleForTest: patchTypeOrmFromModuleExports,
|
|
10132
11432
|
patchMongoDbModuleForTest: patchMongoDbFromModuleExports,
|
|
10133
11433
|
patchMongoCollectionPrototypeForTest: patchMongoCollectionPrototype,
|
|
10134
11434
|
chunkTraceEventsForTransportForTest: chunkTraceEventsForTransport,
|