@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/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 sid = (req.headers['x-bug-session-id'] as string) || querySid || '';
5064
- const aid = (req.headers['x-bug-action-id'] as string) || queryAid || '';
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(cfg).policy ?? null;
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
- cfg,
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
- cfg,
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
- cfg,
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
- cfg,
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
- cfg,
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: cfg,
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: cfg,
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: cfg,
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: cfg,
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: cfg,
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(cfg, sid, {
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(cfg, sid, {
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(cfg, sid, {
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,