@reproapp/node-sdk 0.0.38 → 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;
@@ -5008,14 +5420,193 @@ async function maybeCaptureDbValueFromCandidatesAsync(
5008
5420
  // reproMiddleware — unchanged behavior + passive per-request trace
5009
5421
  // ===================================================================
5010
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;
5011
5425
  /** Configure header capture/masking. Defaults to capturing with sensitive headers masked. */
5012
5426
  captureHeaders?: boolean | HeaderCaptureOptions;
5013
5427
  /** Optional masking rules for request/response payloads and function traces. */
5014
5428
  masking?: ReproMaskingConfig;
5015
5429
  /** Optional privacy policy config for deterministic runtime transformations. */
5016
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>;
5518
+ };
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;
5017
5533
  };
5018
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
+
5019
5610
  export type ReproInitConfig = ReproMiddlewareConfig & {
5020
5611
  /**
5021
5612
  * Tracing configuration for function/http instrumentation.
@@ -5060,20 +5651,65 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5060
5651
  endClientRequest();
5061
5652
  };
5062
5653
 
5063
- const sid = (req.headers['x-bug-session-id'] as string) || querySid || '';
5064
- 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 || '';
5065
5675
  if (!sid || !aid) {
5066
5676
  res.on('finish', endTrackedClientRequest);
5067
5677
  res.on('close', endTrackedClientRequest);
5068
5678
  return next(); // only capture tagged requests
5069
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
+ }
5070
5709
  const resolvePrivacy = (): NormalizedRuntimePrivacyPolicy | null =>
5071
- getRuntimePrivacyState(cfg).policy ?? null;
5710
+ getRuntimePrivacyState(captureCfg).policy ?? null;
5072
5711
 
5073
- const requestStartRaw = Date.now();
5074
- const headerTs = readHeaderNumber(req.headers[REQUEST_START_HEADER]) ?? queryTs;
5075
5712
  const clockSkewMs = headerTs !== null ? headerTs - requestStartRaw : 0;
5076
- const requestEpochMs = headerTs !== null ? headerTs : requestStartRaw + clockSkewMs;
5077
5713
  const rid = nextRequestId(requestEpochMs);
5078
5714
  const captureScope = createRequestCaptureScope(rid, 'http');
5079
5715
  const t0 = requestStartRaw;
@@ -5271,7 +5907,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5271
5907
  ?? await materializeInlinePrivacyValueAsync(
5272
5908
  'request.body',
5273
5909
  sanitizeRequestSnapshot(requestBodyRaw),
5274
- cfg,
5910
+ captureCfg,
5275
5911
  maskReq,
5276
5912
  endpointTraceCtx,
5277
5913
  masking,
@@ -5281,7 +5917,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5281
5917
  const requestParams = await applyPrivacyThenMaskAsync(
5282
5918
  'request.params',
5283
5919
  sanitizeRequestSnapshot((req as any).params),
5284
- cfg,
5920
+ captureCfg,
5285
5921
  maskReq,
5286
5922
  endpointTraceCtx,
5287
5923
  masking,
@@ -5290,7 +5926,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5290
5926
  const requestQuery = await applyPrivacyThenMaskAsync(
5291
5927
  'request.query',
5292
5928
  sanitizeRequestSnapshot((req as any).query),
5293
- cfg,
5929
+ captureCfg,
5294
5930
  maskReq,
5295
5931
  endpointTraceCtx,
5296
5932
  masking,
@@ -5299,7 +5935,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5299
5935
  const maskedHeaders = await applyPrivacyThenMaskAsync(
5300
5936
  'request.headers',
5301
5937
  requestHeaders,
5302
- cfg,
5938
+ captureCfg,
5303
5939
  maskReq,
5304
5940
  endpointTraceCtx,
5305
5941
  masking,
@@ -5311,7 +5947,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5311
5947
  ?? await materializeInlinePrivacyValueAsync(
5312
5948
  'response.body',
5313
5949
  sanitizeRequestSnapshot(capturedBody),
5314
- cfg,
5950
+ captureCfg,
5315
5951
  maskReq,
5316
5952
  endpointTraceCtx,
5317
5953
  masking,
@@ -5326,7 +5962,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5326
5962
  rawValue: (req as any).body,
5327
5963
  previewValue: requestBody,
5328
5964
  capture: {
5329
- runtimeConfig: cfg,
5965
+ runtimeConfig: captureCfg,
5330
5966
  captureHeaders: cfg.captureHeaders,
5331
5967
  maskReq,
5332
5968
  trace: endpointTraceCtx,
@@ -5339,7 +5975,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5339
5975
  rawValue: (req as any).params,
5340
5976
  previewValue: requestParams,
5341
5977
  capture: {
5342
- runtimeConfig: cfg,
5978
+ runtimeConfig: captureCfg,
5343
5979
  captureHeaders: cfg.captureHeaders,
5344
5980
  maskReq,
5345
5981
  trace: endpointTraceCtx,
@@ -5352,7 +5988,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5352
5988
  rawValue: (req as any).query,
5353
5989
  previewValue: requestQuery,
5354
5990
  capture: {
5355
- runtimeConfig: cfg,
5991
+ runtimeConfig: captureCfg,
5356
5992
  captureHeaders: cfg.captureHeaders,
5357
5993
  maskReq,
5358
5994
  trace: endpointTraceCtx,
@@ -5365,7 +6001,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5365
6001
  rawValue: req.headers,
5366
6002
  previewValue: maskedHeaders,
5367
6003
  capture: {
5368
- runtimeConfig: cfg,
6004
+ runtimeConfig: captureCfg,
5369
6005
  captureHeaders: cfg.captureHeaders,
5370
6006
  maskReq,
5371
6007
  trace: endpointTraceCtx,
@@ -5380,7 +6016,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5380
6016
  rawValue: capturedBody,
5381
6017
  previewValue: responseBody,
5382
6018
  capture: {
5383
- runtimeConfig: cfg,
6019
+ runtimeConfig: captureCfg,
5384
6020
  captureHeaders: cfg.captureHeaders,
5385
6021
  maskReq,
5386
6022
  trace: endpointTraceCtx,
@@ -5440,7 +6076,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5440
6076
  chosenEndpoint,
5441
6077
  hasTraceEvents,
5442
6078
  );
5443
- post(cfg, sid, {
6079
+ post(captureCfg, sid, {
5444
6080
  entries: [{
5445
6081
  actionId: aid,
5446
6082
  request: requestPayload,
@@ -5457,7 +6093,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5457
6093
  const emitRequestCaptureManifest = (traceBatchTotal: number): void => {
5458
6094
  if (requestCaptureManifestScheduled) return;
5459
6095
  requestCaptureManifestScheduled = true;
5460
- post(cfg, sid, {
6096
+ post(captureCfg, sid, {
5461
6097
  entries: [{
5462
6098
  actionId: aid,
5463
6099
  requestRid: rid,
@@ -5569,7 +6205,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5569
6205
  try {
5570
6206
  const pendingEvents = preparePendingTraceEventsForFlush(events.slice());
5571
6207
  const materializedEvents = await materializePendingTraceEventsAsync(pendingEvents, {
5572
- cfg,
6208
+ cfg: captureCfg,
5573
6209
  maskReq,
5574
6210
  masking,
5575
6211
  resolvePrivacy,
@@ -5602,7 +6238,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5602
6238
  const batch = traceBatches[i];
5603
6239
  const traceValues = collectBatchTraceValueEntries(batch, i);
5604
6240
 
5605
- post(cfg, sid, {
6241
+ post(captureCfg, sid, {
5606
6242
  entries: [{
5607
6243
  actionId: aid,
5608
6244
  trace: batch,
@@ -5624,6 +6260,15 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5624
6260
 
5625
6261
  sessionDrainWait = waitForSessionDrain(sid);
5626
6262
  endSessionRequest(sid);
6263
+ if (backendLiveState && backendLiveChild) {
6264
+ releaseBackendLiveRequest(
6265
+ cfg,
6266
+ normalizeLiveModeConfig(cfg),
6267
+ backendLiveState,
6268
+ backendLiveChild,
6269
+ finishedAtAligned ?? alignedNow(),
6270
+ );
6271
+ }
5627
6272
  endTrackedClientRequest();
5628
6273
 
5629
6274
  if (__TRACER_READY) {
@@ -10045,9 +10690,11 @@ export async function initRepro(cfg: ReproInitConfig): Promise<void> {
10045
10690
  const ingestCfg = toIngestConfig(cfg);
10046
10691
  reproAutoMiddlewareConfig = {
10047
10692
  ...ingestCfg,
10693
+ apiBase: cfg.apiBase,
10048
10694
  captureHeaders: cfg.captureHeaders,
10049
10695
  masking: cfg.masking,
10050
10696
  privacy: cfg.privacy,
10697
+ liveMode: cfg.liveMode,
10051
10698
  };
10052
10699
  const integrationCfg = {
10053
10700
  ...reproAutoMiddlewareConfig,
@@ -10102,6 +10749,9 @@ export const __reproTestHooks = {
10102
10749
  lastClientActivityAt = 0;
10103
10750
  sdkBackgroundQuietUntil = 0;
10104
10751
  },
10752
+ setBackendLiveTimingForTest(override: Partial<BackendLiveTimingConfig> | null): void {
10753
+ backendLiveTimingOverrideForTest = override;
10754
+ },
10105
10755
  runWithRequestContextForTest<T>(ctx: Partial<Ctx>, fn: () => T): T {
10106
10756
  return als.run({
10107
10757
  ...ctx,