@reproapp/node-sdk 0.0.37 → 0.0.39

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;
@@ -2078,6 +2081,397 @@ function toIngestConfig(cfg: IngestClientConfig): IngestClientConfig {
2078
2081
  };
2079
2082
  }
2080
2083
 
2084
+ function normalizeLiveModeConfig(cfg: ReproMiddlewareConfig): ReproLiveModeConfig | null {
2085
+ const liveMode = cfg.liveMode;
2086
+ if (liveMode === true) {
2087
+ return { enabled: true };
2088
+ }
2089
+ if (!liveMode || typeof liveMode !== 'object') {
2090
+ return null;
2091
+ }
2092
+ if (liveMode.enabled === false) {
2093
+ return null;
2094
+ }
2095
+ return liveMode;
2096
+ }
2097
+
2098
+ type BackendLiveTimingConfig = {
2099
+ sessionMaxDurationMs: number;
2100
+ segmentMaxDurationMs: number;
2101
+ segmentIdleMs: number;
2102
+ };
2103
+
2104
+ let backendLiveTimingOverrideForTest: Partial<BackendLiveTimingConfig> | null = null;
2105
+
2106
+ function positiveInternalDuration(value: unknown, fallback: number): number {
2107
+ const numeric = Number(value);
2108
+ if (!Number.isFinite(numeric) || numeric <= 0) return fallback;
2109
+ return Math.max(1, Math.trunc(numeric));
2110
+ }
2111
+
2112
+ function getBackendLiveTiming(): BackendLiveTimingConfig {
2113
+ return {
2114
+ sessionMaxDurationMs: positiveInternalDuration(
2115
+ backendLiveTimingOverrideForTest?.sessionMaxDurationMs,
2116
+ DEFAULT_LIVE_SESSION_DURATION_MS,
2117
+ ),
2118
+ segmentMaxDurationMs: positiveInternalDuration(
2119
+ backendLiveTimingOverrideForTest?.segmentMaxDurationMs,
2120
+ DEFAULT_LIVE_SEGMENT_DURATION_MS,
2121
+ ),
2122
+ segmentIdleMs: positiveInternalDuration(
2123
+ backendLiveTimingOverrideForTest?.segmentIdleMs,
2124
+ DEFAULT_LIVE_SEGMENT_IDLE_MS,
2125
+ ),
2126
+ };
2127
+ }
2128
+
2129
+ function normalizeLiveActorId(value: unknown): string | null {
2130
+ if (value === undefined || value === null) return null;
2131
+ const normalized = String(value).trim();
2132
+ return normalized.length ? normalized : null;
2133
+ }
2134
+
2135
+ function normalizeLiveActorLabels(
2136
+ value: Record<string, string | number | boolean | null | undefined> | null | undefined,
2137
+ ): Record<string, string> | undefined {
2138
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
2139
+ const labels: Record<string, string> = {};
2140
+ for (const [key, rawValue] of Object.entries(value)) {
2141
+ const normalizedKey = key.trim();
2142
+ if (!normalizedKey || rawValue === undefined || rawValue === null) continue;
2143
+ const normalizedValue = String(rawValue).trim();
2144
+ if (normalizedValue) labels[normalizedKey] = normalizedValue;
2145
+ }
2146
+ return Object.keys(labels).length ? labels : undefined;
2147
+ }
2148
+
2149
+ function newSessionId(base: number = Date.now()): string {
2150
+ return `S_${Math.max(0, Math.trunc(base))}_${randomUUID().slice(0, 8)}`;
2151
+ }
2152
+
2153
+ function newActionId(base: number = Date.now()): string {
2154
+ return `A_${Math.max(0, Math.trunc(base))}_${randomUUID().slice(0, 8)}`;
2155
+ }
2156
+
2157
+ function resolveLiveApiBase(cfg: ReproMiddlewareConfig, liveMode?: ReproLiveModeConfig | null): string | null {
2158
+ const env = typeof process !== 'undefined' ? (process as any)?.env : undefined;
2159
+ const raw =
2160
+ liveMode?.apiBase ||
2161
+ cfg.apiBase ||
2162
+ env?.REPRO_API_BASE_URL ||
2163
+ env?.REPRO_API_BASE ||
2164
+ (cfg.privacy as RuntimePrivacyConfig | undefined)?.apiBase ||
2165
+ null;
2166
+ if (typeof raw !== 'string' || !raw.trim()) return null;
2167
+ return raw.trim().replace(/\/+$/, '');
2168
+ }
2169
+
2170
+ function liveApiHeaders(cfg: ReproMiddlewareConfig): Record<string, string> {
2171
+ const headers: Record<string, string> = {
2172
+ 'Content-Type': 'application/json',
2173
+ 'X-App-Id': cfg.appId,
2174
+ };
2175
+ if (cfg.appSecret) {
2176
+ headers['X-App-Secret'] = cfg.appSecret;
2177
+ }
2178
+ if (cfg.appName) {
2179
+ headers['X-App-Name'] = cfg.appName;
2180
+ }
2181
+ return headers;
2182
+ }
2183
+
2184
+ async function postLiveApi(
2185
+ cfg: ReproMiddlewareConfig,
2186
+ liveMode: ReproLiveModeConfig | null,
2187
+ path: string,
2188
+ body: Record<string, any>,
2189
+ ): Promise<void> {
2190
+ await postLiveApiJson(cfg, liveMode, path, body);
2191
+ }
2192
+
2193
+ async function postLiveApiJson<T = Record<string, any>>(
2194
+ cfg: ReproMiddlewareConfig,
2195
+ liveMode: ReproLiveModeConfig | null,
2196
+ path: string,
2197
+ body: Record<string, any>,
2198
+ ): Promise<T | null> {
2199
+ const apiBase = resolveLiveApiBase(cfg, liveMode);
2200
+ const fetchImpl = (globalThis as any).fetch;
2201
+ if (!apiBase || !cfg.appId || !cfg.appSecret || typeof fetchImpl !== 'function') {
2202
+ return null;
2203
+ }
2204
+ try {
2205
+ const res = await fetchImpl(`${apiBase}${path}`, {
2206
+ method: 'POST',
2207
+ headers: liveApiHeaders(cfg),
2208
+ body: JSON.stringify(body),
2209
+ });
2210
+ if (!res?.ok) {
2211
+ return null;
2212
+ }
2213
+ try {
2214
+ return await res.json();
2215
+ } catch {
2216
+ return null;
2217
+ }
2218
+ } catch {
2219
+ // never break user code
2220
+ return null;
2221
+ }
2222
+ }
2223
+
2224
+ function enqueueLiveSessionStart(
2225
+ cfg: ReproMiddlewareConfig,
2226
+ liveMode: ReproLiveModeConfig | null,
2227
+ payload: Record<string, any>,
2228
+ ): void {
2229
+ scheduleSdkBackgroundWork(
2230
+ () => postLiveApi(cfg, liveMode, '/v1/live-sessions', payload),
2231
+ { priority: 'high' },
2232
+ );
2233
+ }
2234
+
2235
+ function enqueueLiveSessionFinish(
2236
+ cfg: ReproMiddlewareConfig,
2237
+ liveMode: ReproLiveModeConfig | null,
2238
+ sessionId: string,
2239
+ ): void {
2240
+ scheduleSdkBackgroundWork(
2241
+ () => postLiveApi(
2242
+ cfg,
2243
+ liveMode,
2244
+ `/v1/live-sessions/${encodeURIComponent(sessionId)}/finish`,
2245
+ { notes: '' },
2246
+ ),
2247
+ { priority: 'high' },
2248
+ );
2249
+ }
2250
+
2251
+ function emitBackendLiveSessionManifest(
2252
+ cfg: ReproMiddlewareConfig,
2253
+ session: BackendLiveParentSession | BackendLiveChildSession,
2254
+ options: {
2255
+ sessionKind: 'parent' | 'child';
2256
+ actorId: string;
2257
+ actorType: string;
2258
+ actorLabels?: Record<string, string>;
2259
+ parentSessionId?: string | null;
2260
+ previousSessionId?: string | null;
2261
+ closedAt: number;
2262
+ },
2263
+ ): void {
2264
+ post(cfg, session.sessionId, {
2265
+ entries: [{
2266
+ sessionCaptureManifest: {
2267
+ mode: 'backend-only',
2268
+ captureMode: 'backend-only',
2269
+ sessionKind: options.sessionKind,
2270
+ parentSessionId: options.parentSessionId ?? null,
2271
+ previousSessionId: options.previousSessionId ?? null,
2272
+ actorId: options.actorId,
2273
+ actorType: options.actorType,
2274
+ actorLabels: options.actorLabels,
2275
+ expected: {
2276
+ frontendActionDoc: 0,
2277
+ rrwebChunk: 0,
2278
+ frontendSessionEnd: 0,
2279
+ },
2280
+ closedAt: options.closedAt,
2281
+ },
2282
+ t: options.closedAt,
2283
+ }],
2284
+ }, { priority: 'high' });
2285
+ }
2286
+
2287
+ function closeBackendLiveParent(
2288
+ cfg: ReproMiddlewareConfig,
2289
+ liveMode: ReproLiveModeConfig | null,
2290
+ state: BackendLiveActorState,
2291
+ parent: BackendLiveParentSession,
2292
+ closedAt: number,
2293
+ ): void {
2294
+ if (parent.closed) {
2295
+ return;
2296
+ }
2297
+ parent.closed = true;
2298
+ if (parent.closeTimer) {
2299
+ try { clearTimeout(parent.closeTimer); } catch {}
2300
+ parent.closeTimer = null;
2301
+ }
2302
+ state.previousParentSessionId = parent.sessionId;
2303
+ if (state.parent === parent) {
2304
+ state.parent = null;
2305
+ }
2306
+ emitBackendLiveSessionManifest(cfg, parent, {
2307
+ sessionKind: 'parent',
2308
+ actorId: parent.actorId,
2309
+ actorType: parent.actorType,
2310
+ actorLabels: parent.actorLabels,
2311
+ previousSessionId: parent.previousSessionId,
2312
+ closedAt,
2313
+ });
2314
+ enqueueLiveSessionFinish(cfg, liveMode, parent.sessionId);
2315
+ }
2316
+
2317
+ function scheduleBackendLiveParentClose(
2318
+ cfg: ReproMiddlewareConfig,
2319
+ liveMode: ReproLiveModeConfig | null,
2320
+ state: BackendLiveActorState,
2321
+ parent: BackendLiveParentSession,
2322
+ ): void {
2323
+ if (parent.closed) {
2324
+ return;
2325
+ }
2326
+ if (parent.closeTimer) {
2327
+ try { clearTimeout(parent.closeTimer); } catch {}
2328
+ parent.closeTimer = null;
2329
+ }
2330
+
2331
+ const now = Date.now();
2332
+ const activeChild = state.activeChild;
2333
+ const activeChildBelongsToParent =
2334
+ activeChild?.parentSessionId === parent.sessionId;
2335
+ const closeAt =
2336
+ activeChildBelongsToParent && now >= parent.expiresAt
2337
+ ? Math.max(parent.expiresAt, activeChild.closeDeadline, now + 1)
2338
+ : parent.expiresAt;
2339
+
2340
+ parent.closeTimer = setTimeout(() => {
2341
+ parent.closeTimer = null;
2342
+ if (parent.closed || state.parent !== parent) {
2343
+ return;
2344
+ }
2345
+
2346
+ const currentChild = state.activeChild;
2347
+ if (currentChild?.parentSessionId === parent.sessionId) {
2348
+ if (currentChild.requestsInFlight <= 0) {
2349
+ scheduleBackendLiveChildClose(cfg, liveMode, state, currentChild);
2350
+ }
2351
+ scheduleBackendLiveParentClose(cfg, liveMode, state, parent);
2352
+ return;
2353
+ }
2354
+
2355
+ closeBackendLiveParent(cfg, liveMode, state, parent, Date.now());
2356
+ }, Math.max(0, closeAt - now));
2357
+ try { (parent.closeTimer as any).unref?.(); } catch {}
2358
+ }
2359
+
2360
+ function scheduleBackendLiveChildClose(
2361
+ cfg: ReproMiddlewareConfig,
2362
+ liveMode: ReproLiveModeConfig | null,
2363
+ state: BackendLiveActorState,
2364
+ child: BackendLiveChildSession,
2365
+ ): void {
2366
+ if (child.closeTimer) {
2367
+ try { clearTimeout(child.closeTimer); } catch {}
2368
+ child.closeTimer = null;
2369
+ }
2370
+ const delay = Math.max(0, child.closeDeadline - Date.now());
2371
+ child.closeTimer = setTimeout(() => {
2372
+ child.closeTimer = null;
2373
+ if (child.requestsInFlight > 0 || Date.now() < child.closeDeadline) {
2374
+ scheduleBackendLiveChildClose(cfg, liveMode, state, child);
2375
+ return;
2376
+ }
2377
+ if (state.activeChild === child) {
2378
+ state.activeChild = null;
2379
+ }
2380
+ state.previousChildSessionId = child.sessionId;
2381
+ const closedAt = Date.now();
2382
+ emitBackendLiveSessionManifest(cfg, child, {
2383
+ sessionKind: 'child',
2384
+ actorId: child.actorId,
2385
+ actorType: child.actorType,
2386
+ actorLabels: child.actorLabels,
2387
+ parentSessionId: child.parentSessionId,
2388
+ previousSessionId: child.previousSessionId,
2389
+ closedAt,
2390
+ });
2391
+ enqueueLiveSessionFinish(cfg, liveMode, child.sessionId);
2392
+ }, delay);
2393
+ try { (child.closeTimer as any).unref?.(); } catch {}
2394
+ }
2395
+
2396
+ function releaseBackendLiveRequest(
2397
+ cfg: ReproMiddlewareConfig,
2398
+ liveMode: ReproLiveModeConfig | null,
2399
+ state: BackendLiveActorState,
2400
+ child: BackendLiveChildSession,
2401
+ finishedAt: number,
2402
+ ): void {
2403
+ child.requestsInFlight = Math.max(0, child.requestsInFlight - 1);
2404
+ const idleMs = getBackendLiveTiming().segmentIdleMs;
2405
+ child.closeDeadline = Math.max(child.closeDeadline, finishedAt + idleMs);
2406
+ if (child.requestsInFlight <= 0) {
2407
+ scheduleBackendLiveChildClose(cfg, liveMode, state, child);
2408
+ }
2409
+ }
2410
+
2411
+ async function resolveBackendLiveContext(
2412
+ cfg: ReproMiddlewareConfig,
2413
+ req: Request,
2414
+ requestStart: number,
2415
+ ): Promise<BackendLiveResolvedContext | null> {
2416
+ const liveMode = normalizeLiveModeConfig(cfg);
2417
+ if (!liveMode?.backendOnly) {
2418
+ return null;
2419
+ }
2420
+
2421
+ const method = String(req.method || 'GET').toUpperCase();
2422
+ if (method === 'OPTIONS' && !hasReproRequestCorrelation(req)) {
2423
+ return null;
2424
+ }
2425
+ const identity = await resolveLiveActorIdentity(cfg, req);
2426
+ if (!identity) {
2427
+ return null;
2428
+ }
2429
+ const { actorId, actorType, actorLabels } = identity;
2430
+
2431
+ const claim = await postLiveApiJson<BackendLiveClaimResponse>(
2432
+ cfg,
2433
+ liveMode,
2434
+ '/v1/live-sessions/claim',
2435
+ {
2436
+ clientTime: requestStart,
2437
+ captureMode: 'backend-only',
2438
+ actorId,
2439
+ actorIds: [actorId],
2440
+ actorType,
2441
+ actorLabels,
2442
+ },
2443
+ );
2444
+ const sid =
2445
+ normalizeLiveActorId(claim?.childSessionId) ??
2446
+ normalizeLiveActorId(claim?.sessionId);
2447
+ if (!sid) {
2448
+ return null;
2449
+ }
2450
+
2451
+ const aid = normalizeLiveActorId(claim?.actionId) ?? newActionId(requestStart);
2452
+ const parentSessionId = normalizeLiveActorId(claim?.parentSessionId) ?? '';
2453
+ const liveChild: BackendLiveChildSession = {
2454
+ sessionId: sid,
2455
+ parentSessionId,
2456
+ previousSessionId: null,
2457
+ actorId,
2458
+ actorType,
2459
+ actorLabels,
2460
+ startedAt: requestStart,
2461
+ expiresAt: requestStart,
2462
+ closeDeadline: requestStart,
2463
+ requestsInFlight: 0,
2464
+ closeTimer: null,
2465
+ };
2466
+
2467
+ return {
2468
+ sid: liveChild.sessionId,
2469
+ aid,
2470
+ requestStart,
2471
+ child: liveChild,
2472
+ };
2473
+ }
2474
+
2081
2475
  function resolveIngestUrl(cfg: IngestClientConfig): string {
2082
2476
  const envBase = typeof process !== 'undefined' ? (process as any)?.env?.REPRO_INGEST_BASE : undefined;
2083
2477
  const ingestBase = String(envBase || cfg.ingestBase || DEFAULT_INGEST_BASE).replace(/\/+$/, '');
@@ -2125,6 +2519,24 @@ function readRequestQueryNumber(req: Request, name: string): number | null {
2125
2519
  return Number.isFinite(parsed) ? parsed : null;
2126
2520
  }
2127
2521
 
2522
+ function hasReproRequestCorrelation(req: Request): boolean {
2523
+ const headers = req.headers || {};
2524
+ if (
2525
+ hasHttpHeader(headers, KAFKA_HEADER_SESSION_ID) ||
2526
+ hasHttpHeader(headers, KAFKA_HEADER_ACTION_ID) ||
2527
+ hasHttpHeader(headers, KAFKA_HEADER_REQUEST_ID) ||
2528
+ hasHttpHeader(headers, KAFKA_HEADER_TRACE_ID) ||
2529
+ hasHttpHeader(headers, KAFKA_HEADER_SPAN_ID) ||
2530
+ hasHttpHeader(headers, KAFKA_HEADER_PARENT_SPAN_ID)
2531
+ ) {
2532
+ return true;
2533
+ }
2534
+ return Boolean(
2535
+ readRequestQueryParam(req, REPRO_QUERY_SESSION_PARAM) ||
2536
+ readRequestQueryParam(req, REPRO_QUERY_ACTION_PARAM),
2537
+ );
2538
+ }
2539
+
2128
2540
  function stripReproQueryFromUrl(rawUrl: string): string {
2129
2541
  if (!rawUrl || !rawUrl.includes('?')) {
2130
2542
  return rawUrl;
@@ -4955,18 +5367,246 @@ async function maybeCaptureDbValueAsync(
4955
5367
  return result.captureRef;
4956
5368
  }
4957
5369
 
5370
+ function pushUniqueCaptureCandidate(candidates: any[], value: any): void {
5371
+ if (value === undefined) return;
5372
+ if (candidates.some((candidate) => candidate === value)) return;
5373
+ candidates.push(value);
5374
+ }
5375
+
5376
+ function buildResultMetaFullValueCandidates(resultMetaRaw: any, resultMetaSource: any): any[] {
5377
+ const candidates: any[] = [];
5378
+ pushUniqueCaptureCandidate(candidates, resultMetaRaw);
5379
+ if (
5380
+ resultMetaSource &&
5381
+ typeof resultMetaSource === 'object' &&
5382
+ Object.prototype.hasOwnProperty.call(resultMetaSource, 'result')
5383
+ ) {
5384
+ pushUniqueCaptureCandidate(candidates, resultMetaSource.result);
5385
+ }
5386
+ pushUniqueCaptureCandidate(candidates, resultMetaSource);
5387
+ return candidates;
5388
+ }
5389
+
5390
+ async function maybeCaptureDbValueFromCandidatesAsync(
5391
+ params: {
5392
+ target: 'db.pk' | 'db.before' | 'db.after' | 'db.query' | 'db.resultMeta' | 'db.error';
5393
+ rawValues: any[];
5394
+ previewValue: any;
5395
+ capture: FullValueCaptureContext;
5396
+ },
5397
+ sink: TraceValueBatchEntry[],
5398
+ ): Promise<TraceValueCaptureRef | undefined> {
5399
+ let skippedRef: TraceValueCaptureRef | undefined;
5400
+ for (const rawValue of params.rawValues) {
5401
+ const result = await createCapturedValueEntryAsync({
5402
+ target: params.target,
5403
+ rawValue,
5404
+ previewValue: params.previewValue,
5405
+ capture: params.capture,
5406
+ });
5407
+ if (!result) continue;
5408
+ if (result.entry) {
5409
+ sink.push(result.entry);
5410
+ return result.captureRef;
5411
+ }
5412
+ if (!skippedRef) {
5413
+ skippedRef = result.captureRef;
5414
+ }
5415
+ }
5416
+ return skippedRef;
5417
+ }
5418
+
4958
5419
  // ===================================================================
4959
5420
  // reproMiddleware — unchanged behavior + passive per-request trace
4960
5421
  // ===================================================================
4961
5422
  export type ReproMiddlewareConfig = IngestClientConfig & {
5423
+ /** API base used for backend-owned live session documents. Defaults to REPRO_API_BASE_URL/REPRO_API_BASE/privacy.apiBase. */
5424
+ apiBase?: string;
4962
5425
  /** Configure header capture/masking. Defaults to capturing with sensitive headers masked. */
4963
5426
  captureHeaders?: boolean | HeaderCaptureOptions;
4964
5427
  /** Optional masking rules for request/response payloads and function traces. */
4965
5428
  masking?: ReproMaskingConfig;
4966
5429
  /** Optional privacy policy config for deterministic runtime transformations. */
4967
5430
  privacy?: RuntimePrivacyConfig;
5431
+ /** Opt-in production/live capture mode. Staging/manual tagged capture is unchanged when disabled. */
5432
+ liveMode?: ReproLiveModeConfig | boolean;
5433
+ };
5434
+
5435
+ export type ReproLiveActorArgs = {
5436
+ req: Request;
5437
+ headers: Request['headers'];
5438
+ method: string;
5439
+ url: string;
5440
+ path: string;
5441
+ ip?: string | null;
5442
+ appId: string;
5443
+ appName?: string;
5444
+ serviceName?: string;
5445
+ };
5446
+
5447
+ export type ReproLiveActorDecision = ReproLiveActorArgs & {
5448
+ actorId: string;
5449
+ actorType?: string;
5450
+ actorLabels?: Record<string, string>;
5451
+ };
5452
+
5453
+ export type ReproLiveModeConfig = {
5454
+ enabled?: boolean;
5455
+ backendOnly?: boolean;
5456
+ /**
5457
+ * Stable actor/correlation key for live capture. Return null/undefined/empty
5458
+ * to skip capture for that request.
5459
+ */
5460
+ defineActorId?: (
5461
+ args: ReproLiveActorArgs,
5462
+ ) => string | number | null | undefined | Promise<string | number | null | undefined>;
5463
+ /**
5464
+ * Optional filter for segmenting which actors/requests are captured.
5465
+ */
5466
+ shouldCapture?: (
5467
+ args: ReproLiveActorDecision,
5468
+ ) => boolean | Promise<boolean>;
5469
+ actorType?: string;
5470
+ actorLabels?:
5471
+ | Record<string, string | number | boolean | null | undefined>
5472
+ | ((args: ReproLiveActorDecision) =>
5473
+ | Record<string, string | number | boolean | null | undefined>
5474
+ | null
5475
+ | undefined
5476
+ | Promise<Record<string, string | number | boolean | null | undefined> | null | undefined>);
5477
+ apiBase?: string;
5478
+ };
5479
+
5480
+ type BackendLiveParentSession = {
5481
+ sessionId: string;
5482
+ startedAt: number;
5483
+ expiresAt: number;
5484
+ previousSessionId: string | null;
5485
+ actorId: string;
5486
+ actorType: string;
5487
+ actorLabels?: Record<string, string>;
5488
+ closeTimer?: NodeJS.Timeout | null;
5489
+ closed?: boolean;
5490
+ };
5491
+
5492
+ type BackendLiveChildSession = {
5493
+ sessionId: string;
5494
+ parentSessionId: string;
5495
+ previousSessionId: string | null;
5496
+ actorId: string;
5497
+ actorType: string;
5498
+ actorLabels?: Record<string, string>;
5499
+ startedAt: number;
5500
+ expiresAt: number;
5501
+ closeDeadline: number;
5502
+ requestsInFlight: number;
5503
+ closeTimer?: NodeJS.Timeout | null;
5504
+ };
5505
+
5506
+ type BackendLiveActorState = {
5507
+ actorId: string;
5508
+ parent: BackendLiveParentSession | null;
5509
+ previousParentSessionId: string | null;
5510
+ activeChild: BackendLiveChildSession | null;
5511
+ previousChildSessionId: string | null;
5512
+ };
5513
+
5514
+ type BackendLiveActorIdentity = {
5515
+ actorId: string;
5516
+ actorType: string;
5517
+ actorLabels?: Record<string, string>;
4968
5518
  };
4969
5519
 
5520
+ type BackendLiveResolvedContext = {
5521
+ sid: string;
5522
+ aid: string;
5523
+ requestStart: number;
5524
+ child: BackendLiveChildSession;
5525
+ };
5526
+
5527
+ type BackendLiveClaimResponse = {
5528
+ parentSessionId?: string | null;
5529
+ childSessionId?: string | null;
5530
+ sessionId?: string | null;
5531
+ actionId?: string | null;
5532
+ clockOffsetMs?: number | null;
5533
+ };
5534
+
5535
+ function buildLiveActorArgs(cfg: ReproMiddlewareConfig, req: Request): ReproLiveActorArgs {
5536
+ const rawUrl = String((req as any).originalUrl || req.url || '/');
5537
+ const path = rawUrl.split('?')[0] || '/';
5538
+ return {
5539
+ req,
5540
+ headers: req.headers,
5541
+ method: String(req.method || 'GET').toUpperCase(),
5542
+ url: rawUrl,
5543
+ path,
5544
+ ip: (req as any).ip ?? (req as any).socket?.remoteAddress ?? null,
5545
+ appId: cfg.appId,
5546
+ appName: cfg.appName,
5547
+ serviceName: cfg.serviceName,
5548
+ };
5549
+ }
5550
+
5551
+ async function resolveLiveActorIdentity(
5552
+ cfg: ReproMiddlewareConfig,
5553
+ req: Request,
5554
+ ): Promise<BackendLiveActorIdentity | null> {
5555
+ const liveMode = normalizeLiveModeConfig(cfg);
5556
+ if (!liveMode) {
5557
+ return null;
5558
+ }
5559
+
5560
+ const args = buildLiveActorArgs(cfg, req);
5561
+ let rawActorId: string | number | null | undefined;
5562
+ try {
5563
+ rawActorId = liveMode.defineActorId
5564
+ ? await liveMode.defineActorId(args)
5565
+ : cfg.actorId;
5566
+ } catch {
5567
+ rawActorId = null;
5568
+ }
5569
+ const actorId = normalizeLiveActorId(rawActorId);
5570
+ if (!actorId) {
5571
+ return null;
5572
+ }
5573
+
5574
+ const actorType = normalizeLiveActorId(liveMode.actorType) ?? cfg.actorType ?? 'user';
5575
+ const decision: ReproLiveActorDecision = {
5576
+ ...args,
5577
+ actorId,
5578
+ actorType,
5579
+ };
5580
+ if (liveMode.shouldCapture) {
5581
+ let shouldCapture = false;
5582
+ try {
5583
+ shouldCapture = (await liveMode.shouldCapture(decision)) === true;
5584
+ } catch {
5585
+ shouldCapture = false;
5586
+ }
5587
+ if (!shouldCapture) {
5588
+ return null;
5589
+ }
5590
+ }
5591
+
5592
+ let rawLabels:
5593
+ | Record<string, string | number | boolean | null | undefined>
5594
+ | null
5595
+ | undefined;
5596
+ try {
5597
+ rawLabels =
5598
+ typeof liveMode.actorLabels === 'function'
5599
+ ? await liveMode.actorLabels(decision)
5600
+ : liveMode.actorLabels;
5601
+ } catch {
5602
+ rawLabels = undefined;
5603
+ }
5604
+ const actorLabels = normalizeLiveActorLabels(rawLabels) ?? normalizeLiveActorLabels(cfg.actorLabels);
5605
+ return { actorId, actorType, actorLabels };
5606
+ }
5607
+
5608
+ const backendLiveActors = new Map<string, BackendLiveActorState>();
5609
+
4970
5610
  export type ReproInitConfig = ReproMiddlewareConfig & {
4971
5611
  /**
4972
5612
  * Tracing configuration for function/http instrumentation.
@@ -5011,20 +5651,65 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5011
5651
  endClientRequest();
5012
5652
  };
5013
5653
 
5014
- const sid = (req.headers['x-bug-session-id'] as string) || querySid || '';
5015
- const aid = (req.headers['x-bug-action-id'] as string) || queryAid || '';
5654
+ const requestStartRaw = Date.now();
5655
+ const headerTs = readHeaderNumber(req.headers[REQUEST_START_HEADER]) ?? queryTs;
5656
+ const requestEpochMs = headerTs !== null ? headerTs : requestStartRaw;
5657
+ let backendLiveState: BackendLiveActorState | null = null;
5658
+ let backendLiveChild: BackendLiveChildSession | null = null;
5659
+ const headerSid = (req.headers['x-bug-session-id'] as string) || '';
5660
+ const headerAid = (req.headers['x-bug-action-id'] as string) || '';
5661
+ const hasIncomingReproSession = Boolean(headerSid || querySid || headerAid || queryAid);
5662
+ const backendLiveContext =
5663
+ !hasIncomingReproSession
5664
+ ? await resolveBackendLiveContext(cfg, req, requestEpochMs)
5665
+ : null;
5666
+ const taggedActorIdentity = hasIncomingReproSession
5667
+ ? await resolveLiveActorIdentity(cfg, req)
5668
+ : null;
5669
+ if (backendLiveContext) {
5670
+ backendLiveState = backendLiveActors.get(backendLiveContext.child.actorId) ?? null;
5671
+ backendLiveChild = backendLiveContext.child;
5672
+ }
5673
+ const sid = headerSid || querySid || backendLiveContext?.sid || '';
5674
+ const aid = headerAid || queryAid || backendLiveContext?.aid || '';
5016
5675
  if (!sid || !aid) {
5017
5676
  res.on('finish', endTrackedClientRequest);
5018
5677
  res.on('close', endTrackedClientRequest);
5019
5678
  return next(); // only capture tagged requests
5020
5679
  }
5680
+ const captureCfg: ReproMiddlewareConfig = backendLiveChild
5681
+ ? {
5682
+ ...cfg,
5683
+ actorId: backendLiveChild.actorId,
5684
+ actorType: backendLiveChild.actorType,
5685
+ actorLabels: backendLiveChild.actorLabels,
5686
+ }
5687
+ : taggedActorIdentity
5688
+ ? {
5689
+ ...cfg,
5690
+ actorId: taggedActorIdentity.actorId,
5691
+ actorType: taggedActorIdentity.actorType,
5692
+ actorLabels: taggedActorIdentity.actorLabels,
5693
+ }
5694
+ : cfg;
5695
+ if (taggedActorIdentity) {
5696
+ post(captureCfg, sid, {
5697
+ entries: [{
5698
+ actionId: aid,
5699
+ sessionActor: {
5700
+ actorId: taggedActorIdentity.actorId,
5701
+ actorType: taggedActorIdentity.actorType,
5702
+ actorLabels: taggedActorIdentity.actorLabels,
5703
+ source: 'node-sdk',
5704
+ },
5705
+ t: requestEpochMs,
5706
+ }],
5707
+ }, { priority: 'high' });
5708
+ }
5021
5709
  const resolvePrivacy = (): NormalizedRuntimePrivacyPolicy | null =>
5022
- getRuntimePrivacyState(cfg).policy ?? null;
5710
+ getRuntimePrivacyState(captureCfg).policy ?? null;
5023
5711
 
5024
- const requestStartRaw = Date.now();
5025
- const headerTs = readHeaderNumber(req.headers[REQUEST_START_HEADER]) ?? queryTs;
5026
5712
  const clockSkewMs = headerTs !== null ? headerTs - requestStartRaw : 0;
5027
- const requestEpochMs = headerTs !== null ? headerTs : requestStartRaw + clockSkewMs;
5028
5713
  const rid = nextRequestId(requestEpochMs);
5029
5714
  const captureScope = createRequestCaptureScope(rid, 'http');
5030
5715
  const t0 = requestStartRaw;
@@ -5222,7 +5907,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5222
5907
  ?? await materializeInlinePrivacyValueAsync(
5223
5908
  'request.body',
5224
5909
  sanitizeRequestSnapshot(requestBodyRaw),
5225
- cfg,
5910
+ captureCfg,
5226
5911
  maskReq,
5227
5912
  endpointTraceCtx,
5228
5913
  masking,
@@ -5232,7 +5917,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5232
5917
  const requestParams = await applyPrivacyThenMaskAsync(
5233
5918
  'request.params',
5234
5919
  sanitizeRequestSnapshot((req as any).params),
5235
- cfg,
5920
+ captureCfg,
5236
5921
  maskReq,
5237
5922
  endpointTraceCtx,
5238
5923
  masking,
@@ -5241,7 +5926,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5241
5926
  const requestQuery = await applyPrivacyThenMaskAsync(
5242
5927
  'request.query',
5243
5928
  sanitizeRequestSnapshot((req as any).query),
5244
- cfg,
5929
+ captureCfg,
5245
5930
  maskReq,
5246
5931
  endpointTraceCtx,
5247
5932
  masking,
@@ -5250,7 +5935,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5250
5935
  const maskedHeaders = await applyPrivacyThenMaskAsync(
5251
5936
  'request.headers',
5252
5937
  requestHeaders,
5253
- cfg,
5938
+ captureCfg,
5254
5939
  maskReq,
5255
5940
  endpointTraceCtx,
5256
5941
  masking,
@@ -5262,7 +5947,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5262
5947
  ?? await materializeInlinePrivacyValueAsync(
5263
5948
  'response.body',
5264
5949
  sanitizeRequestSnapshot(capturedBody),
5265
- cfg,
5950
+ captureCfg,
5266
5951
  maskReq,
5267
5952
  endpointTraceCtx,
5268
5953
  masking,
@@ -5277,7 +5962,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5277
5962
  rawValue: (req as any).body,
5278
5963
  previewValue: requestBody,
5279
5964
  capture: {
5280
- runtimeConfig: cfg,
5965
+ runtimeConfig: captureCfg,
5281
5966
  captureHeaders: cfg.captureHeaders,
5282
5967
  maskReq,
5283
5968
  trace: endpointTraceCtx,
@@ -5290,7 +5975,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5290
5975
  rawValue: (req as any).params,
5291
5976
  previewValue: requestParams,
5292
5977
  capture: {
5293
- runtimeConfig: cfg,
5978
+ runtimeConfig: captureCfg,
5294
5979
  captureHeaders: cfg.captureHeaders,
5295
5980
  maskReq,
5296
5981
  trace: endpointTraceCtx,
@@ -5303,7 +5988,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5303
5988
  rawValue: (req as any).query,
5304
5989
  previewValue: requestQuery,
5305
5990
  capture: {
5306
- runtimeConfig: cfg,
5991
+ runtimeConfig: captureCfg,
5307
5992
  captureHeaders: cfg.captureHeaders,
5308
5993
  maskReq,
5309
5994
  trace: endpointTraceCtx,
@@ -5316,7 +6001,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5316
6001
  rawValue: req.headers,
5317
6002
  previewValue: maskedHeaders,
5318
6003
  capture: {
5319
- runtimeConfig: cfg,
6004
+ runtimeConfig: captureCfg,
5320
6005
  captureHeaders: cfg.captureHeaders,
5321
6006
  maskReq,
5322
6007
  trace: endpointTraceCtx,
@@ -5331,7 +6016,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5331
6016
  rawValue: capturedBody,
5332
6017
  previewValue: responseBody,
5333
6018
  capture: {
5334
- runtimeConfig: cfg,
6019
+ runtimeConfig: captureCfg,
5335
6020
  captureHeaders: cfg.captureHeaders,
5336
6021
  maskReq,
5337
6022
  trace: endpointTraceCtx,
@@ -5391,7 +6076,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5391
6076
  chosenEndpoint,
5392
6077
  hasTraceEvents,
5393
6078
  );
5394
- post(cfg, sid, {
6079
+ post(captureCfg, sid, {
5395
6080
  entries: [{
5396
6081
  actionId: aid,
5397
6082
  request: requestPayload,
@@ -5408,7 +6093,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5408
6093
  const emitRequestCaptureManifest = (traceBatchTotal: number): void => {
5409
6094
  if (requestCaptureManifestScheduled) return;
5410
6095
  requestCaptureManifestScheduled = true;
5411
- post(cfg, sid, {
6096
+ post(captureCfg, sid, {
5412
6097
  entries: [{
5413
6098
  actionId: aid,
5414
6099
  requestRid: rid,
@@ -5520,7 +6205,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5520
6205
  try {
5521
6206
  const pendingEvents = preparePendingTraceEventsForFlush(events.slice());
5522
6207
  const materializedEvents = await materializePendingTraceEventsAsync(pendingEvents, {
5523
- cfg,
6208
+ cfg: captureCfg,
5524
6209
  maskReq,
5525
6210
  masking,
5526
6211
  resolvePrivacy,
@@ -5553,7 +6238,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5553
6238
  const batch = traceBatches[i];
5554
6239
  const traceValues = collectBatchTraceValueEntries(batch, i);
5555
6240
 
5556
- post(cfg, sid, {
6241
+ post(captureCfg, sid, {
5557
6242
  entries: [{
5558
6243
  actionId: aid,
5559
6244
  trace: batch,
@@ -5575,6 +6260,15 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5575
6260
 
5576
6261
  sessionDrainWait = waitForSessionDrain(sid);
5577
6262
  endSessionRequest(sid);
6263
+ if (backendLiveState && backendLiveChild) {
6264
+ releaseBackendLiveRequest(
6265
+ cfg,
6266
+ normalizeLiveModeConfig(cfg),
6267
+ backendLiveState,
6268
+ backendLiveChild,
6269
+ finishedAtAligned ?? alignedNow(),
6270
+ );
6271
+ }
5578
6272
  endTrackedClientRequest();
5579
6273
 
5580
6274
  if (__TRACER_READY) {
@@ -6241,9 +6935,9 @@ async function emitDbQueryAsync(cfg: any, sid?: string, aid?: string, payload?:
6241
6935
  previewValue: queryPreview,
6242
6936
  capture,
6243
6937
  }, dbValues);
6244
- const resultMetaValueCapture = resultMetaMaterialization.skipped ? undefined : await maybeCaptureDbValueAsync({
6938
+ const resultMetaValueCapture = resultMetaMaterialization.skipped ? undefined : await maybeCaptureDbValueFromCandidatesAsync({
6245
6939
  target: 'db.resultMeta',
6246
- rawValue: payload.resultMetaRaw ?? resultMetaSource,
6940
+ rawValues: buildResultMetaFullValueCandidates(payload.resultMetaRaw, resultMetaSource),
6247
6941
  previewValue: resultMetaPreview,
6248
6942
  capture,
6249
6943
  }, dbValues);
@@ -9996,9 +10690,11 @@ export async function initRepro(cfg: ReproInitConfig): Promise<void> {
9996
10690
  const ingestCfg = toIngestConfig(cfg);
9997
10691
  reproAutoMiddlewareConfig = {
9998
10692
  ...ingestCfg,
10693
+ apiBase: cfg.apiBase,
9999
10694
  captureHeaders: cfg.captureHeaders,
10000
10695
  masking: cfg.masking,
10001
10696
  privacy: cfg.privacy,
10697
+ liveMode: cfg.liveMode,
10002
10698
  };
10003
10699
  const integrationCfg = {
10004
10700
  ...reproAutoMiddlewareConfig,
@@ -10053,6 +10749,9 @@ export const __reproTestHooks = {
10053
10749
  lastClientActivityAt = 0;
10054
10750
  sdkBackgroundQuietUntil = 0;
10055
10751
  },
10752
+ setBackendLiveTimingForTest(override: Partial<BackendLiveTimingConfig> | null): void {
10753
+ backendLiveTimingOverrideForTest = override;
10754
+ },
10056
10755
  runWithRequestContextForTest<T>(ctx: Partial<Ctx>, fn: () => T): T {
10057
10756
  return als.run({
10058
10757
  ...ctx,