@reproapp/react-sdk 0.0.2 → 0.0.3

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/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from "react";
3
- import { record } from "rrweb";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { EventType, record } from 'rrweb';
4
4
  import { gzip } from 'pako';
5
5
  // config
6
6
  const MAX_BYTES = 900 * 1024; // 900 KB target per POST (tune)
@@ -34,6 +34,7 @@ function splitEventsBySize(events, mkEnvelope) {
34
34
  // ---- small helpers ----
35
35
  const now = () => Date.now();
36
36
  const newAID = () => `A_${now()}_${Math.random().toString(36).slice(2, 6)}`;
37
+ const newSID = () => `S_${now()}_${Math.random().toString(36).slice(2, 10)}`;
37
38
  // server time normalization
38
39
  const offsetRef = { current: 0 }; // ms to add to client time
39
40
  const nowServer = () => now() + (offsetRef.current || 0);
@@ -43,11 +44,63 @@ async function getJSON(url, headers) {
43
44
  throw new Error(`${r.status}`);
44
45
  return (await r.json());
45
46
  }
46
- const INTERNAL_HEADER = "X-Repro-Internal";
47
- const REQUEST_START_HEADER = "X-Bug-Request-Start";
48
- const NGROK_SKIP_HEADER = "ngrok-skip-browser-warning";
49
- const NGROK_SKIP_VALUE = "true";
50
- const API_BASE = "https://repro-api-d7288.ondigitalocean.app/api";
47
+ const INTERNAL_HEADER = 'X-Repro-Internal';
48
+ const REQUEST_START_HEADER = 'X-Bug-Request-Start';
49
+ const REPRO_QUERY_SESSION_PARAM = '__repro_sid';
50
+ const REPRO_QUERY_ACTION_PARAM = '__repro_aid';
51
+ const REPRO_QUERY_START_PARAM = '__repro_start';
52
+ const NGROK_SKIP_HEADER = 'ngrok-skip-browser-warning';
53
+ const NGROK_SKIP_VALUE = 'true';
54
+ const API_BASE = 'https://repro-api-d7288.ondigitalocean.app/api';
55
+ const INGEST_BASE = 'https://log-collector-5rj86.ondigitalocean.app';
56
+ const INGEST_PATH = '/v1/ingest/events';
57
+ const readSdkEnv = (key) => {
58
+ if (typeof globalThis === 'undefined') {
59
+ return undefined;
60
+ }
61
+ const globals = globalThis;
62
+ const sdkEnv = globals.__REPRO_SDK_ENV__;
63
+ if (sdkEnv && typeof sdkEnv === 'object' && typeof sdkEnv[key] === 'string') {
64
+ return sdkEnv[key];
65
+ }
66
+ const processEnv = globals.process?.env;
67
+ if (processEnv && typeof processEnv === 'object' && typeof processEnv[key] === 'string') {
68
+ return processEnv[key];
69
+ }
70
+ return undefined;
71
+ };
72
+ const readPositiveEnvNumber = (key, fallback) => {
73
+ const raw = readSdkEnv(key);
74
+ const parsed = Number(raw ?? fallback);
75
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
76
+ };
77
+ const LIVE_PARENT_MAX_MS = readPositiveEnvNumber('REPRO_LIVE_PARENT_MAX_MS', 2 * 60 * 1000);
78
+ const LIVE_REQUEST_PREROLL_MS = readPositiveEnvNumber('REPRO_LIVE_REQUEST_PREROLL_MS', 2000);
79
+ const LIVE_REQUEST_POSTROLL_MS = readPositiveEnvNumber('REPRO_LIVE_REQUEST_POSTROLL_MS', 2000);
80
+ const LIVE_SEGMENT_IDLE_MS = readPositiveEnvNumber('REPRO_LIVE_SEGMENT_IDLE_MS', 2000);
81
+ const LIVE_RECENT_BUFFER_MAX_EVENTS = readPositiveEnvNumber('REPRO_LIVE_RECENT_BUFFER_MAX_EVENTS', 400);
82
+ const LIVE_RRWEB_CHECKOUT_MS = readPositiveEnvNumber('REPRO_LIVE_RRWEB_CHECKOUT_MS', 2000);
83
+ const SDK_QUEUE_IDLE_DELAY_MS = readPositiveEnvNumber('REPRO_SDK_QUEUE_IDLE_DELAY_MS', 25);
84
+ function normalizeActorLabels(labels) {
85
+ if (!labels) {
86
+ return undefined;
87
+ }
88
+ const out = {};
89
+ for (const [key, value] of Object.entries(labels)) {
90
+ if (!key || typeof value !== 'string') {
91
+ continue;
92
+ }
93
+ out[key] = value;
94
+ }
95
+ return Object.keys(out).length ? out : undefined;
96
+ }
97
+ function normalizeIngestMode(mode) {
98
+ if (mode === 'api')
99
+ return 'live';
100
+ if (mode === 'log-collector')
101
+ return 'staging';
102
+ return mode ?? 'live';
103
+ }
51
104
  let __reproCtx = null;
52
105
  /** Manually attach Repro to any Axios instance (no recursion). */
53
106
  export function attachAxios(axiosInstance) {
@@ -58,90 +111,209 @@ export function attachAxios(axiosInstance) {
58
111
  const ctx = __reproCtx;
59
112
  if (!ctx)
60
113
  return config;
61
- const sid = ctx.getSid();
62
- const aid = ctx.getAid();
63
- const url = `${config.baseURL || ""}${config.url || ""}`;
114
+ const url = `${config.baseURL || ''}${config.url || ''}`;
64
115
  const isInternal = url.startsWith(ctx.base);
65
116
  const isSdkInternal = !!config.headers?.[INTERNAL_HEADER];
117
+ const isClientRequest = !isInternal && !isSdkInternal;
66
118
  if (!config.headers)
67
119
  config.headers = {};
120
+ const existingHeaders = new Headers(config.headers);
68
121
  const setHeader = (key, value) => {
69
122
  if (!config.headers)
70
123
  return;
71
- if (typeof config.headers.set === "function") {
124
+ if (typeof config.headers.set === 'function') {
72
125
  config.headers.set(key, value);
73
126
  }
74
127
  else {
75
128
  config.headers[key] = value;
76
129
  }
77
130
  };
78
- if (!isSdkInternal) {
79
- const requestStart = nowServer();
131
+ let requestStart = nowServer();
132
+ let sid = ctx.getSid();
133
+ let aid = ctx.getAid();
134
+ let label = null;
135
+ if (ctx.resolveRequestContext) {
136
+ const resolved = ctx.resolveRequestContext({
137
+ url,
138
+ isInternal,
139
+ isSdkInternal,
140
+ method: config.method,
141
+ });
142
+ requestStart = resolved.requestStart;
143
+ sid = resolved.sessionId;
144
+ aid = resolved.actionId;
145
+ label = resolved.label ?? null;
146
+ }
147
+ const useQueryPropagation = sid && aid
148
+ ? shouldUseQueryPropagation({
149
+ url,
150
+ method: config.method,
151
+ headers: existingHeaders,
152
+ isInternal,
153
+ isSdkInternal,
154
+ })
155
+ : false;
156
+ if (sid && aid && !isSdkInternal && !useQueryPropagation) {
80
157
  setHeader(REQUEST_START_HEADER, String(requestStart));
81
158
  }
82
159
  const sdkToken = ctx.getToken();
83
160
  const userToken = ctx.getUserToken();
84
161
  if (isInternal) {
85
162
  setHeader(NGROK_SKIP_HEADER, NGROK_SKIP_VALUE);
86
- if (sdkToken && !config.headers["x-sdk-token"]) {
87
- setHeader("x-sdk-token", sdkToken);
163
+ if (sdkToken && !config.headers['x-sdk-token']) {
164
+ setHeader('x-sdk-token', sdkToken);
88
165
  }
89
- const existingAuth = config.headers?.Authorization ??
90
- config.headers?.authorization;
166
+ const existingAuth = config.headers?.Authorization ?? config.headers?.authorization;
91
167
  if (userToken && !existingAuth) {
92
- setHeader("Authorization", `Bearer ${userToken}`);
168
+ setHeader('Authorization', `Bearer ${userToken}`);
93
169
  }
94
170
  }
95
- if (sid && aid && !isInternal && !isSdkInternal) {
96
- setHeader("X-Bug-Session-Id", sid);
97
- setHeader("X-Bug-Action-Id", aid);
171
+ if (sid && aid && !isInternal && !isSdkInternal && useQueryPropagation) {
172
+ config.url = appendReproQuery(url, { sessionId: sid, actionId: aid, requestStart });
173
+ if (config.baseURL) {
174
+ config.baseURL = undefined;
175
+ }
176
+ }
177
+ else if (sid && aid && !isInternal && !isSdkInternal) {
178
+ setHeader('X-Bug-Session-Id', sid);
179
+ setHeader('X-Bug-Action-Id', aid);
180
+ }
181
+ config.__reproSessionId = sid ?? null;
182
+ config.__reproActionId = aid ?? null;
183
+ config.__reproActionLabel = label;
184
+ if (isClientRequest) {
185
+ ctx.beginClientRequest?.();
186
+ config.__reproClientRequest = true;
98
187
  }
99
188
  return config;
100
189
  });
101
- axiosInstance.interceptors.response.use(async (resp) => {
190
+ axiosInstance.interceptors.response.use((resp) => {
102
191
  const ctx = __reproCtx;
103
192
  if (!ctx)
104
193
  return resp;
105
- const url = `${resp.config.baseURL || ""}${resp.config.url || ""}`;
194
+ const url = `${resp.config.baseURL || ''}${resp.config.url || ''}`;
106
195
  const isInternal = url.startsWith(ctx.base);
107
196
  const isSdkInternal = !!resp.config.headers?.[INTERNAL_HEADER];
108
197
  if (isInternal && resp.status === 401) {
109
198
  ctx.onUnauthorized?.();
110
199
  }
111
- const sid = ctx.getSid();
112
- const aid = ctx.getAid();
113
- if (!isInternal && !isSdkInternal && sid && aid && !ctx.hasReqMarked.has(aid)) {
114
- ctx.hasReqMarked.add(aid);
200
+ const sid = resp?.config?.__reproSessionId ?? ctx.getSid();
201
+ const aid = resp?.config?.__reproActionId ?? ctx.getAid();
202
+ const label = resp?.config?.__reproActionLabel ?? null;
203
+ const hasReqKey = sid && aid ? `${sid}:${aid}` : null;
204
+ if (!isInternal && !isSdkInternal && sid && aid && hasReqKey && !ctx.hasReqMarked.has(hasReqKey)) {
205
+ if (ctx.notifyRequestCompleted) {
206
+ ctx.notifyRequestCompleted({
207
+ sessionId: sid,
208
+ actionId: aid,
209
+ label,
210
+ finishedAt: nowServer(),
211
+ });
212
+ }
213
+ }
214
+ if (resp?.config?.__reproClientRequest) {
215
+ ctx.endClientRequest?.();
115
216
  }
116
217
  return resp;
117
218
  }, (err) => {
118
219
  const ctx = __reproCtx;
119
220
  if (ctx) {
120
221
  const status = err?.response?.status;
121
- const url = `${err?.config?.baseURL || ""}${err?.config?.url || ""}`;
222
+ const url = `${err?.config?.baseURL || ''}${err?.config?.url || ''}`;
122
223
  if (status === 401 && url.startsWith(ctx.base)) {
123
224
  ctx.onUnauthorized?.();
124
225
  }
226
+ const requestConfig = err?.config ?? err?.response?.config;
227
+ if (requestConfig?.__reproClientRequest) {
228
+ ctx.endClientRequest?.();
229
+ }
125
230
  }
126
231
  return Promise.reject(err);
127
232
  });
128
233
  }
129
- export function ReproProvider({ appId, children, button, masking, apiBase }) {
234
+ const isCrossOriginUrl = (url) => {
235
+ if (typeof window === 'undefined') {
236
+ return false;
237
+ }
238
+ try {
239
+ return new URL(url, window.location.href).origin !== window.location.origin;
240
+ }
241
+ catch {
242
+ return false;
243
+ }
244
+ };
245
+ const isCorsSafelistedContentType = (value) => {
246
+ const contentType = value.split(';', 1)[0]?.trim().toLowerCase();
247
+ return (contentType === 'application/x-www-form-urlencoded' ||
248
+ contentType === 'multipart/form-data' ||
249
+ contentType === 'text/plain');
250
+ };
251
+ const hasNonSafelistedCorsHeaders = (headers) => {
252
+ let hasNonSafelisted = false;
253
+ headers.forEach((rawValue, rawName) => {
254
+ const name = rawName.toLowerCase();
255
+ if (name === 'accept' || name === 'accept-language' || name === 'content-language') {
256
+ return;
257
+ }
258
+ if (name === 'content-type' && isCorsSafelistedContentType(rawValue)) {
259
+ return;
260
+ }
261
+ hasNonSafelisted = true;
262
+ });
263
+ return hasNonSafelisted;
264
+ };
265
+ const shouldUseQueryPropagation = (params) => {
266
+ if (params.isInternal || params.isSdkInternal || !isCrossOriginUrl(params.url)) {
267
+ return false;
268
+ }
269
+ const method = String(params.method || 'GET').toUpperCase();
270
+ if (method !== 'GET' && method !== 'HEAD' && method !== 'POST') {
271
+ return false;
272
+ }
273
+ return !hasNonSafelistedCorsHeaders(params.headers);
274
+ };
275
+ const appendReproQuery = (url, params) => {
276
+ const parsed = new URL(url, typeof window !== 'undefined' ? window.location.href : 'http://localhost');
277
+ parsed.searchParams.set(REPRO_QUERY_SESSION_PARAM, params.sessionId);
278
+ parsed.searchParams.set(REPRO_QUERY_ACTION_PARAM, params.actionId);
279
+ parsed.searchParams.set(REPRO_QUERY_START_PARAM, String(params.requestStart));
280
+ return parsed.toString();
281
+ };
282
+ export function ReproProvider({ appId, tenantId, children, button, masking, apiBase, logCollectorUrl, logCollectorUri, ingest, }) {
130
283
  const base = apiBase?.trim() ? apiBase.trim() : API_BASE;
284
+ const ingestMode = normalizeIngestMode(ingest?.mode);
285
+ const isLiveMode = ingestMode === 'live';
286
+ const isStagingMode = ingestMode === 'staging';
287
+ const useIngestPipeline = ingestMode === 'staging' || ingestMode === 'live';
288
+ const envIngestBase = readSdkEnv('REPRO_LOG_COLLECTOR_URL') ||
289
+ readSdkEnv('REPRO_LOG_COLLECTOR_URI') ||
290
+ readSdkEnv('REPRO_INGEST_BASE');
291
+ const ingestBase = String(envIngestBase ||
292
+ logCollectorUrl ||
293
+ logCollectorUri ||
294
+ ingest?.logCollectorUrl ||
295
+ ingest?.logCollectorUri ||
296
+ ingest?.baseUrl ||
297
+ INGEST_BASE).replace(/\/+$/, '');
298
+ const ingestPath = ingest?.path || INGEST_PATH;
299
+ const normalizedIngestPath = ingestPath.startsWith('/') ? ingestPath : `/${ingestPath}`;
300
+ const ingestUrl = `${ingestBase}${normalizedIngestPath}`;
301
+ const resolvedTenantId = (tenantId && tenantId.trim().length ? tenantId : appId).trim();
302
+ const actorLabels = normalizeActorLabels(ingest?.actorLabels);
131
303
  const storageKey = `repro-auth-${appId}`;
132
304
  const initialAuth = (() => {
133
- if (typeof window === "undefined")
305
+ if (typeof window === 'undefined')
134
306
  return null;
135
307
  try {
136
308
  const raw = window.localStorage.getItem(storageKey);
137
309
  if (!raw)
138
310
  return null;
139
311
  const parsed = JSON.parse(raw);
140
- if (!parsed || typeof parsed !== "object")
312
+ if (!parsed || typeof parsed !== 'object')
141
313
  return null;
142
- const email = typeof parsed.email === "string" ? parsed.email : null;
143
- const password = typeof parsed.password === "string" ? parsed.password : null;
144
- const token = typeof parsed.token === "string" && parsed.token.trim().length
314
+ const email = typeof parsed.email === 'string' ? parsed.email : null;
315
+ const password = typeof parsed.password === 'string' ? parsed.password : null;
316
+ const token = typeof parsed.token === 'string' && parsed.token.trim().length
145
317
  ? parsed.token.trim()
146
318
  : null;
147
319
  if (!email || !token)
@@ -161,6 +333,14 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
161
333
  // ---- refs & state (hooks MUST be inside component) ----
162
334
  const sdkTokenRef = useRef(null);
163
335
  const sessionIdRef = useRef(null);
336
+ const parentSessionRef = useRef(null);
337
+ const activeChildSessionRef = useRef(null);
338
+ const previousChildSessionIdRef = useRef(null);
339
+ const liveParentRotationPendingRef = useRef(false);
340
+ const liveChildCloseTimerRef = useRef(null);
341
+ const liveRecentEventsRef = useRef([]);
342
+ const liveLatestMetaEventRef = useRef(null);
343
+ const liveSinceFullSnapshotRef = useRef([]);
164
344
  const stopRecRef = useRef();
165
345
  const rrwebEventsRef = useRef([]);
166
346
  const currentAidRef = useRef(null);
@@ -174,6 +354,11 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
174
354
  const isFlushingRef = useRef(false);
175
355
  const backoffRef = useRef(0); // ms
176
356
  const isStoppingRef = useRef(false);
357
+ const sdkQueueRef = useRef([]);
358
+ const sdkQueueTimerRef = useRef(null);
359
+ const sdkQueueDrainingRef = useRef(false);
360
+ const sdkQueueBackoffRef = useRef(0);
361
+ const clientRequestsInFlightRef = useRef(0);
177
362
  // NEW: track installed click handler + dedupe recent clicks
178
363
  const clickHandlerRef = useRef(null);
179
364
  const lastClickRef = useRef(null);
@@ -184,13 +369,13 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
184
369
  const userPasswordRef = useRef(initialAuth?.password ?? null);
185
370
  const userTokenRef = useRef(initialAuth?.token ?? null);
186
371
  const [showLogin, setShowLogin] = useState(false);
187
- const [loginEmail, setLoginEmail] = useState(initialAuth?.email ?? "");
188
- const [loginPassword, setLoginPassword] = useState(initialAuth?.password ?? "");
372
+ const [loginEmail, setLoginEmail] = useState(initialAuth?.email ?? '');
373
+ const [loginPassword, setLoginPassword] = useState(initialAuth?.password ?? '');
189
374
  const [loginError, setLoginError] = useState(null);
190
375
  const [isLoggingIn, setIsLoggingIn] = useState(false);
191
376
  const disableLogin = isLoggingIn || !loginEmail.trim() || !loginPassword.trim();
192
377
  const [shareUrl, setShareUrl] = useState(null);
193
- const [copyStatus, setCopyStatus] = useState("idle");
378
+ const [copyStatus, setCopyStatus] = useState('idle');
194
379
  const copyStatusTimerRef = useRef(null);
195
380
  const logoutInFlightRef = useRef(false);
196
381
  const authCheckInFlightRef = useRef(false);
@@ -201,7 +386,7 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
201
386
  const clearCopyStatusTimer = () => {
202
387
  if (copyStatusTimerRef.current == null)
203
388
  return;
204
- if (typeof window !== "undefined") {
389
+ if (typeof window !== 'undefined') {
205
390
  window.clearTimeout(copyStatusTimerRef.current);
206
391
  }
207
392
  else {
@@ -211,7 +396,7 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
211
396
  };
212
397
  const resetCopyFeedback = () => {
213
398
  clearCopyStatusTimer();
214
- setCopyStatus("idle");
399
+ setCopyStatus('idle');
215
400
  };
216
401
  const clearShareInfo = () => {
217
402
  setShareUrl(null);
@@ -220,11 +405,11 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
220
405
  const setCopyStatusWithTimeout = (status) => {
221
406
  clearCopyStatusTimer();
222
407
  setCopyStatus(status);
223
- if (status === "idle")
408
+ if (status === 'idle')
224
409
  return;
225
- if (typeof window !== "undefined") {
410
+ if (typeof window !== 'undefined') {
226
411
  copyStatusTimerRef.current = window.setTimeout(() => {
227
- setCopyStatus("idle");
412
+ setCopyStatus('idle');
228
413
  copyStatusTimerRef.current = null;
229
414
  }, 2200);
230
415
  }
@@ -232,7 +417,7 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
232
417
  const clearSessionExpiryTimer = () => {
233
418
  if (sessionExpiryTimerRef.current == null)
234
419
  return;
235
- if (typeof window !== "undefined") {
420
+ if (typeof window !== 'undefined') {
236
421
  window.clearTimeout(sessionExpiryTimerRef.current);
237
422
  }
238
423
  else {
@@ -240,6 +425,17 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
240
425
  }
241
426
  sessionExpiryTimerRef.current = null;
242
427
  };
428
+ const clearLiveChildCloseTimer = () => {
429
+ if (liveChildCloseTimerRef.current == null)
430
+ return;
431
+ if (typeof window !== 'undefined') {
432
+ window.clearTimeout(liveChildCloseTimerRef.current);
433
+ }
434
+ else {
435
+ clearTimeout(liveChildCloseTimerRef.current);
436
+ }
437
+ liveChildCloseTimerRef.current = null;
438
+ };
243
439
  const addTenantHeader = (headers) => ({
244
440
  ...headers,
245
441
  [NGROK_SKIP_HEADER]: NGROK_SKIP_VALUE,
@@ -249,6 +445,512 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
249
445
  headers.set(NGROK_SKIP_HEADER, NGROK_SKIP_VALUE);
250
446
  }
251
447
  };
448
+ const createSdkSession = async (payload) => {
449
+ if (!sdkTokenRef.current) {
450
+ return null;
451
+ }
452
+ const fetcher = origFetchRef.current ?? window.fetch.bind(window);
453
+ const r = await fetcher(`${base}/v1/sessions`, {
454
+ method: 'POST',
455
+ headers: addTenantHeader({
456
+ 'Content-Type': 'application/json',
457
+ 'x-sdk-token': sdkTokenRef.current,
458
+ ...(userTokenRef.current ? { Authorization: `Bearer ${userTokenRef.current}` } : {}),
459
+ [INTERNAL_HEADER]: '1',
460
+ }),
461
+ body: JSON.stringify(payload),
462
+ });
463
+ if (!r.ok) {
464
+ handleUnauthorizedStatus(r.status);
465
+ return null;
466
+ }
467
+ return (await r.json());
468
+ };
469
+ const finishSdkSession = async (sid) => {
470
+ if (!sdkTokenRef.current) {
471
+ return null;
472
+ }
473
+ const origFetch = origFetchRef.current ?? window.fetch.bind(window);
474
+ const res = await origFetch(`${base}/v1/sessions/${sid}/finish`, {
475
+ method: 'POST',
476
+ headers: addTenantHeader({
477
+ 'Content-Type': 'application/json',
478
+ 'x-sdk-token': sdkTokenRef.current,
479
+ ...(userTokenRef.current ? { Authorization: `Bearer ${userTokenRef.current}` } : {}),
480
+ [INTERNAL_HEADER]: '1',
481
+ }),
482
+ body: JSON.stringify({ notes: '' }),
483
+ });
484
+ if (handleUnauthorizedStatus(res.status)) {
485
+ return null;
486
+ }
487
+ if (!res.ok) {
488
+ return null;
489
+ }
490
+ try {
491
+ return await res.json();
492
+ }
493
+ catch {
494
+ return null;
495
+ }
496
+ };
497
+ const hasClientRequestsInFlight = () => clientRequestsInFlightRef.current > 0;
498
+ const scheduleSdkQueueDrain = (delayMs = 0) => {
499
+ if (sdkQueueTimerRef.current != null || typeof window === 'undefined') {
500
+ return;
501
+ }
502
+ const nextDelay = hasClientRequestsInFlight()
503
+ ? Math.max(delayMs, SDK_QUEUE_IDLE_DELAY_MS)
504
+ : delayMs;
505
+ sdkQueueTimerRef.current = window.setTimeout(() => {
506
+ sdkQueueTimerRef.current = null;
507
+ if (hasClientRequestsInFlight()) {
508
+ scheduleSdkQueueDrain(SDK_QUEUE_IDLE_DELAY_MS);
509
+ return;
510
+ }
511
+ void drainSdkQueue();
512
+ }, nextDelay);
513
+ };
514
+ const beginClientRequest = () => {
515
+ clientRequestsInFlightRef.current += 1;
516
+ };
517
+ const endClientRequest = () => {
518
+ clientRequestsInFlightRef.current = Math.max(0, clientRequestsInFlightRef.current - 1);
519
+ if (!hasClientRequestsInFlight() && sdkQueueRef.current.length > 0) {
520
+ scheduleSdkQueueDrain(SDK_QUEUE_IDLE_DELAY_MS);
521
+ }
522
+ };
523
+ const enqueueSdkTask = (task) => {
524
+ sdkQueueRef.current.push({ ...task, attempts: 0 });
525
+ if (sdkQueueRef.current.length > 500) {
526
+ sdkQueueRef.current.splice(0, sdkQueueRef.current.length - 500);
527
+ }
528
+ scheduleSdkQueueDrain(SDK_QUEUE_IDLE_DELAY_MS);
529
+ };
530
+ const drainSdkQueue = async (options) => {
531
+ if (sdkQueueDrainingRef.current) {
532
+ return;
533
+ }
534
+ const force = options?.force === true;
535
+ if (!force && hasClientRequestsInFlight()) {
536
+ scheduleSdkQueueDrain(SDK_QUEUE_IDLE_DELAY_MS);
537
+ return;
538
+ }
539
+ sdkQueueDrainingRef.current = true;
540
+ try {
541
+ while (sdkQueueRef.current.length > 0) {
542
+ if (!force && hasClientRequestsInFlight()) {
543
+ scheduleSdkQueueDrain(SDK_QUEUE_IDLE_DELAY_MS);
544
+ return;
545
+ }
546
+ const task = sdkQueueRef.current[0];
547
+ const ok = await task.run().catch(() => false);
548
+ if (ok) {
549
+ sdkQueueRef.current.shift();
550
+ sdkQueueBackoffRef.current = 0;
551
+ continue;
552
+ }
553
+ task.attempts += 1;
554
+ if (task.attempts >= task.maxAttempts) {
555
+ sdkQueueRef.current.shift();
556
+ sdkQueueBackoffRef.current = 0;
557
+ continue;
558
+ }
559
+ sdkQueueBackoffRef.current = Math.min(4000, sdkQueueBackoffRef.current ? sdkQueueBackoffRef.current * 2 : 250);
560
+ scheduleSdkQueueDrain(sdkQueueBackoffRef.current);
561
+ return;
562
+ }
563
+ }
564
+ finally {
565
+ sdkQueueDrainingRef.current = false;
566
+ if (sdkQueueRef.current.length > 0 && sdkQueueTimerRef.current == null) {
567
+ scheduleSdkQueueDrain(sdkQueueBackoffRef.current || 0);
568
+ }
569
+ }
570
+ };
571
+ const waitForSdkQueueDrain = async () => {
572
+ while (sdkQueueDrainingRef.current) {
573
+ await new Promise((resolve) => {
574
+ if (typeof window === 'undefined') {
575
+ setTimeout(resolve, 10);
576
+ }
577
+ else {
578
+ window.setTimeout(resolve, 10);
579
+ }
580
+ });
581
+ }
582
+ };
583
+ const clearSdkQueueTimer = () => {
584
+ if (sdkQueueTimerRef.current != null && typeof window !== 'undefined') {
585
+ window.clearTimeout(sdkQueueTimerRef.current);
586
+ sdkQueueTimerRef.current = null;
587
+ }
588
+ };
589
+ const flushSdkQueue = async () => {
590
+ clearSdkQueueTimer();
591
+ await waitForSdkQueueDrain();
592
+ clearSdkQueueTimer();
593
+ await drainSdkQueue({ force: true });
594
+ };
595
+ const enqueueCanonicalEvents = (events) => {
596
+ if (!events.length) {
597
+ return;
598
+ }
599
+ enqueueSdkTask({
600
+ maxAttempts: 5,
601
+ run: async () => {
602
+ const result = await postCanonicalEventsNow(events);
603
+ return result === 'ok' || result === 'too_large';
604
+ },
605
+ });
606
+ };
607
+ const enqueueCreateSdkSession = (payload, onCreated) => {
608
+ enqueueSdkTask({
609
+ maxAttempts: 5,
610
+ run: async () => {
611
+ const created = await createSdkSession(payload);
612
+ if (!created) {
613
+ return false;
614
+ }
615
+ onCreated?.(created);
616
+ return true;
617
+ },
618
+ });
619
+ };
620
+ const enqueueFinishSdkSession = (sid) => {
621
+ enqueueSdkTask({
622
+ maxAttempts: 5,
623
+ run: async () => {
624
+ await finishSdkSession(sid);
625
+ return true;
626
+ },
627
+ });
628
+ };
629
+ const trimLiveRecentEvents = () => {
630
+ const floor = nowServer() - Math.max(LIVE_REQUEST_PREROLL_MS, LIVE_SEGMENT_IDLE_MS);
631
+ while (liveRecentEventsRef.current.length > LIVE_RECENT_BUFFER_MAX_EVENTS ||
632
+ (liveRecentEventsRef.current.length > 0 &&
633
+ Number(liveRecentEventsRef.current[0]?.timestamp ?? 0) < floor)) {
634
+ liveRecentEventsRef.current.shift();
635
+ }
636
+ };
637
+ const scheduleLiveParentExpiry = (parent) => {
638
+ clearSessionExpiryTimer();
639
+ if (!isLiveMode) {
640
+ return;
641
+ }
642
+ const delay = Math.max(0, parent.expiresAt - nowServer());
643
+ sessionExpiryTimerRef.current = window.setTimeout(() => {
644
+ const currentParent = parentSessionRef.current;
645
+ if (!currentParent || currentParent.sessionId !== parent.sessionId) {
646
+ return;
647
+ }
648
+ if (activeChildSessionRef.current) {
649
+ liveParentRotationPendingRef.current = true;
650
+ return;
651
+ }
652
+ parentSessionRef.current = null;
653
+ sessionIdRef.current = null;
654
+ enqueueFinishSdkSession(parent.sessionId);
655
+ if (recordingRef.current && sdkTokenRef.current) {
656
+ ensureLiveParentSession();
657
+ }
658
+ }, delay);
659
+ };
660
+ const ensureLiveParentSession = () => {
661
+ if (!sdkTokenRef.current) {
662
+ return null;
663
+ }
664
+ const existing = parentSessionRef.current;
665
+ const nowTs = nowServer();
666
+ if (existing && nowTs < existing.expiresAt && !liveParentRotationPendingRef.current) {
667
+ return existing;
668
+ }
669
+ if (existing && activeChildSessionRef.current) {
670
+ liveParentRotationPendingRef.current = true;
671
+ return existing;
672
+ }
673
+ if (existing) {
674
+ enqueueFinishSdkSession(existing.sessionId);
675
+ }
676
+ const sessionId = newSID();
677
+ enqueueCreateSdkSession({
678
+ sessionId,
679
+ clientTime: nowTs,
680
+ sessionKind: 'parent',
681
+ }, (created) => {
682
+ if (parentSessionRef.current?.sessionId === created.sessionId) {
683
+ offsetRef.current = Number(created.clockOffsetMs || 0);
684
+ }
685
+ });
686
+ const parent = {
687
+ sessionId,
688
+ startedAt: nowTs,
689
+ expiresAt: nowTs + LIVE_PARENT_MAX_MS,
690
+ };
691
+ parentSessionRef.current = parent;
692
+ sessionIdRef.current = parent.sessionId;
693
+ liveParentRotationPendingRef.current = false;
694
+ scheduleLiveParentExpiry(parent);
695
+ return parent;
696
+ };
697
+ const scheduleLiveChildCloseCheck = () => {
698
+ clearLiveChildCloseTimer();
699
+ const child = activeChildSessionRef.current;
700
+ if (!child || child.requestsInFlight > 0) {
701
+ return;
702
+ }
703
+ const delay = Math.max(0, child.closeDeadline - nowServer());
704
+ if (typeof window !== 'undefined') {
705
+ liveChildCloseTimerRef.current = window.setTimeout(() => {
706
+ void finalizeLiveChildSession();
707
+ }, delay);
708
+ }
709
+ };
710
+ const ensureFrontendActionInSession = (sessionId, action) => {
711
+ const child = activeChildSessionRef.current;
712
+ if (child && child.sessionId === sessionId) {
713
+ const actionKey = `${sessionId}:${action.aid}`;
714
+ if (!child.postedActionKeys.has(actionKey)) {
715
+ child.postedActionKeys.add(actionKey);
716
+ postFrontendActionEvent(sessionId, action);
717
+ return;
718
+ }
719
+ if (action.hasReq || action.hasDb || action.error || typeof action.tEnd === 'number') {
720
+ postFrontendActionEvent(sessionId, action);
721
+ }
722
+ return;
723
+ }
724
+ postFrontendActionEvent(sessionId, action);
725
+ };
726
+ const sendRrwebEventsToIngest = async (sid, events, seqStart = 1) => {
727
+ if (!events.length) {
728
+ return seqStart;
729
+ }
730
+ const mkEnvelope = (slice) => {
731
+ const tFirst = slice[0]?.timestamp ?? nowServer();
732
+ const tLast = slice[slice.length - 1]?.timestamp ?? tFirst;
733
+ return { type: 'rrweb', seq: seqStart, tFirst, tLast, events: slice };
734
+ };
735
+ const pieces = splitEventsBySize(events, mkEnvelope);
736
+ for (let i = 0; i < pieces.length; i += 1) {
737
+ const piece = pieces[i];
738
+ const env = mkEnvelope(piece);
739
+ const pieceSeq = seqStart + i;
740
+ await sendChunkToLogCollector({
741
+ sid,
742
+ envelope: env,
743
+ seq: pieceSeq,
744
+ });
745
+ }
746
+ return seqStart + pieces.length;
747
+ };
748
+ const finalizeLiveParentIfNeeded = async () => {
749
+ if (activeChildSessionRef.current) {
750
+ return;
751
+ }
752
+ if (!parentSessionRef.current) {
753
+ return;
754
+ }
755
+ if (liveParentRotationPendingRef.current || nowServer() >= parentSessionRef.current.expiresAt) {
756
+ const parent = parentSessionRef.current;
757
+ parentSessionRef.current = null;
758
+ sessionIdRef.current = null;
759
+ liveParentRotationPendingRef.current = false;
760
+ clearSessionExpiryTimer();
761
+ enqueueFinishSdkSession(parent.sessionId);
762
+ if (recordingRef.current && sdkTokenRef.current) {
763
+ ensureLiveParentSession();
764
+ }
765
+ }
766
+ };
767
+ const finalizeDetachedLiveChildSession = async (child) => {
768
+ try {
769
+ const nextSeq = await sendRrwebEventsToIngest(child.sessionId, child.events, child.nextSeq);
770
+ child.nextSeq = nextSeq;
771
+ postSessionEndEvent(child.sessionId, {
772
+ finishedAt: nowServer(),
773
+ rrwebNextSeq: child.nextSeq,
774
+ });
775
+ enqueueFinishSdkSession(child.sessionId);
776
+ }
777
+ catch {
778
+ /* best-effort */
779
+ }
780
+ finally {
781
+ await finalizeLiveParentIfNeeded();
782
+ }
783
+ };
784
+ const finalizeLiveChildSession = async () => {
785
+ const child = activeChildSessionRef.current;
786
+ if (!child) {
787
+ return;
788
+ }
789
+ if (child.requestsInFlight > 0 || nowServer() < child.closeDeadline) {
790
+ scheduleLiveChildCloseCheck();
791
+ return;
792
+ }
793
+ clearLiveChildCloseTimer();
794
+ activeChildSessionRef.current = null;
795
+ previousChildSessionIdRef.current = child.sessionId;
796
+ await finalizeDetachedLiveChildSession(child);
797
+ };
798
+ const ensureLiveChildSession = (requestStart) => {
799
+ const active = activeChildSessionRef.current;
800
+ const nowTs = nowServer();
801
+ if (active && (active.requestsInFlight > 0 || nowTs <= active.closeDeadline)) {
802
+ active.requestsInFlight += 1;
803
+ active.closeDeadline = Math.max(active.closeDeadline, requestStart + LIVE_REQUEST_POSTROLL_MS + LIVE_SEGMENT_IDLE_MS);
804
+ scheduleLiveChildCloseCheck();
805
+ return active;
806
+ }
807
+ if (active) {
808
+ clearLiveChildCloseTimer();
809
+ activeChildSessionRef.current = null;
810
+ previousChildSessionIdRef.current = active.sessionId;
811
+ void finalizeDetachedLiveChildSession(active);
812
+ }
813
+ const parent = ensureLiveParentSession();
814
+ if (!parent) {
815
+ return null;
816
+ }
817
+ const sessionId = newSID();
818
+ enqueueCreateSdkSession({
819
+ sessionId,
820
+ clientTime: requestStart,
821
+ sessionKind: 'child',
822
+ parentSessionId: parent.sessionId,
823
+ previousSessionId: previousChildSessionIdRef.current,
824
+ });
825
+ const preRollFloor = requestStart - LIVE_REQUEST_PREROLL_MS;
826
+ const preRollEvents = liveRecentEventsRef.current.filter((ev) => Number(ev?.timestamp ?? 0) >= preRollFloor);
827
+ const hasRecentFullSnapshot = preRollEvents.some((ev) => Number(ev?.type) === EventType.FullSnapshot);
828
+ const snapshotSeed = hasRecentFullSnapshot
829
+ ? preRollEvents.slice()
830
+ : [
831
+ ...(liveLatestMetaEventRef.current &&
832
+ !liveSinceFullSnapshotRef.current.some((ev) => Number(ev?.type) === EventType.Meta)
833
+ ? [liveLatestMetaEventRef.current]
834
+ : []),
835
+ ...liveSinceFullSnapshotRef.current,
836
+ ];
837
+ const child = {
838
+ sessionId,
839
+ parentSessionId: parent.sessionId,
840
+ previousSessionId: previousChildSessionIdRef.current,
841
+ startedAt: requestStart,
842
+ closeDeadline: requestStart + LIVE_REQUEST_POSTROLL_MS + LIVE_SEGMENT_IDLE_MS,
843
+ requestsInFlight: 1,
844
+ events: (snapshotSeed.length ? snapshotSeed : preRollEvents).slice(),
845
+ postedActionKeys: new Set(),
846
+ nextSeq: 1,
847
+ };
848
+ activeChildSessionRef.current = child;
849
+ scheduleLiveChildCloseCheck();
850
+ return child;
851
+ };
852
+ const resolveRequestLabel = (method, url) => {
853
+ const normalizedMethod = String(method || 'GET').toUpperCase();
854
+ if (!url) {
855
+ return `${normalizedMethod} request`;
856
+ }
857
+ try {
858
+ const parsed = new URL(url, typeof window !== 'undefined' ? window.location.origin : 'http://localhost');
859
+ return `${normalizedMethod} ${parsed.pathname}`;
860
+ }
861
+ catch {
862
+ return `${normalizedMethod} ${url}`;
863
+ }
864
+ };
865
+ const resolveOutgoingRequestContext = (params) => {
866
+ const requestStart = nowServer();
867
+ if (params.isInternal || params.isSdkInternal) {
868
+ return {
869
+ sessionId: isLiveMode ? parentSessionRef.current?.sessionId ?? null : sessionIdRef.current,
870
+ actionId: currentAidRef.current,
871
+ requestStart,
872
+ label: lastActionLabelRef.current,
873
+ };
874
+ }
875
+ if (!isLiveMode) {
876
+ return {
877
+ sessionId: sessionIdRef.current,
878
+ actionId: currentAidRef.current,
879
+ requestStart,
880
+ label: lastActionLabelRef.current,
881
+ };
882
+ }
883
+ const child = ensureLiveChildSession(requestStart);
884
+ if (!child) {
885
+ return {
886
+ sessionId: null,
887
+ actionId: null,
888
+ requestStart,
889
+ label: null,
890
+ };
891
+ }
892
+ const actionId = currentAidRef.current ?? newAID();
893
+ const label = lastActionLabelRef.current ?? resolveRequestLabel(params.method, params.url);
894
+ const existingMeta = actionMeta.current.get(actionId);
895
+ if (!existingMeta) {
896
+ actionMeta.current.set(actionId, { tStart: requestStart, label });
897
+ }
898
+ else {
899
+ actionMeta.current.set(actionId, {
900
+ tStart: Math.min(existingMeta.tStart, requestStart),
901
+ label: existingMeta.label ?? label,
902
+ });
903
+ }
904
+ ensureFrontendActionInSession(child.sessionId, {
905
+ aid: actionId,
906
+ label,
907
+ tStart: requestStart,
908
+ tEnd: requestStart,
909
+ hasReq: false,
910
+ hasDb: false,
911
+ error: false,
912
+ ui: { kind: currentAidRef.current ? 'click' : 'request' },
913
+ });
914
+ return {
915
+ sessionId: child.sessionId,
916
+ actionId,
917
+ requestStart,
918
+ label,
919
+ };
920
+ };
921
+ const notifyRequestCompleted = (params) => {
922
+ const finishedAt = Number.isFinite(Number(params.finishedAt))
923
+ ? Number(params.finishedAt)
924
+ : nowServer();
925
+ const sessionId = params.sessionId;
926
+ const actionId = params.actionId;
927
+ if (!sessionId || !actionId) {
928
+ return;
929
+ }
930
+ const meta = actionMeta.current.get(actionId);
931
+ const actionStart = meta?.tStart ?? finishedAt;
932
+ const actionLabel = params.label ?? meta?.label ?? lastActionLabelRef.current;
933
+ const hasReqKey = `${sessionId}:${actionId}`;
934
+ if (!hasReqMarkedRef.current.has(hasReqKey)) {
935
+ hasReqMarkedRef.current.add(hasReqKey);
936
+ ensureFrontendActionInSession(sessionId, {
937
+ aid: actionId,
938
+ label: actionLabel,
939
+ tStart: actionStart,
940
+ tEnd: finishedAt,
941
+ hasReq: true,
942
+ ui: {},
943
+ });
944
+ }
945
+ if (isLiveMode) {
946
+ const child = activeChildSessionRef.current;
947
+ if (child && child.sessionId === sessionId) {
948
+ child.requestsInFlight = Math.max(0, child.requestsInFlight - 1);
949
+ child.closeDeadline = Math.max(child.closeDeadline, finishedAt + LIVE_REQUEST_POSTROLL_MS + LIVE_SEGMENT_IDLE_MS);
950
+ scheduleLiveChildCloseCheck();
951
+ }
952
+ }
953
+ };
252
954
  useEffect(() => {
253
955
  __reproCtx = {
254
956
  base,
@@ -257,14 +959,18 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
257
959
  getToken: () => sdkTokenRef.current,
258
960
  getUserToken: () => userTokenRef.current,
259
961
  getUserPassword: () => userPasswordRef.current,
260
- getFetch: () => (origFetchRef.current ?? window.fetch.bind(window)),
962
+ getFetch: () => origFetchRef.current ?? window.fetch.bind(window),
261
963
  hasReqMarked: hasReqMarkedRef.current,
964
+ resolveRequestContext: resolveOutgoingRequestContext,
965
+ notifyRequestCompleted,
966
+ beginClientRequest,
967
+ endClientRequest,
262
968
  onUnauthorized: () => handleUnauthorized(),
263
969
  };
264
970
  return () => {
265
971
  __reproCtx = null;
266
972
  };
267
- }, [base]);
973
+ }, [base, isLiveMode]);
268
974
  useEffect(() => {
269
975
  const storedToken = initialAuthRef.current?.token ?? null;
270
976
  if (!storedToken) {
@@ -279,8 +985,7 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
279
985
  if (auth?.token !== storedToken) {
280
986
  return;
281
987
  }
282
- if (authCheckInFlightRef.current ||
283
- lastAuthCheckTokenRef.current === storedToken) {
988
+ if (authCheckInFlightRef.current || lastAuthCheckTokenRef.current === storedToken) {
284
989
  return;
285
990
  }
286
991
  let cancelled = false;
@@ -290,12 +995,12 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
290
995
  try {
291
996
  const fetcher = origFetchRef.current ?? window.fetch.bind(window);
292
997
  const resp = await fetcher(`${base}/v1/apps/${appId}/users/me`, {
293
- method: "GET",
998
+ method: 'GET',
294
999
  headers: addTenantHeader({
295
- Accept: "application/json",
1000
+ Accept: 'application/json',
296
1001
  Authorization: `Bearer ${storedToken}`,
297
- "x-sdk-token": sdkToken,
298
- [INTERNAL_HEADER]: "1",
1002
+ 'x-sdk-token': sdkToken,
1003
+ [INTERNAL_HEADER]: '1',
299
1004
  }),
300
1005
  });
301
1006
  if (cancelled)
@@ -320,7 +1025,7 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
320
1025
  useEffect(() => {
321
1026
  userPasswordRef.current = auth?.password ?? null;
322
1027
  userTokenRef.current = auth?.token ?? null;
323
- if (typeof window !== "undefined") {
1028
+ if (typeof window !== 'undefined') {
324
1029
  try {
325
1030
  if (auth) {
326
1031
  window.localStorage.setItem(storageKey, JSON.stringify(auth));
@@ -346,11 +1051,11 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
346
1051
  setLoginError(null);
347
1052
  try {
348
1053
  const resp = await fetch(`${base}/v1/apps/${appId}/users/login`, {
349
- method: "POST",
1054
+ method: 'POST',
350
1055
  headers: addTenantHeader({
351
- Accept: "application/json",
352
- "Content-Type": "application/json",
353
- [INTERNAL_HEADER]: "1",
1056
+ Accept: 'application/json',
1057
+ 'Content-Type': 'application/json',
1058
+ [INTERNAL_HEADER]: '1',
354
1059
  }),
355
1060
  body: JSON.stringify({ email, password }),
356
1061
  });
@@ -358,22 +1063,20 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
358
1063
  throw new Error(`Login failed (${resp.status})`);
359
1064
  }
360
1065
  const data = await resp.json();
361
- const accessTokenFromUser = typeof data?.user?.accessToken === "string"
1066
+ const accessTokenFromUser = typeof data?.user?.accessToken === 'string'
362
1067
  ? data.user.accessToken.trim()
363
1068
  : null;
364
- const accessTokenFromData = typeof data?.accessToken === "string"
365
- ? data.accessToken.trim()
366
- : null;
1069
+ const accessTokenFromData = typeof data?.accessToken === 'string' ? data.accessToken.trim() : null;
367
1070
  const accessToken = accessTokenFromUser || accessTokenFromData;
368
1071
  if (!accessToken) {
369
- throw new Error("Login response did not include an access token.");
1072
+ throw new Error('Login response did not include an access token.');
370
1073
  }
371
1074
  setAuth({ email, password, token: accessToken, data });
372
- setLoginPassword("");
1075
+ setLoginPassword('');
373
1076
  setShowLogin(false);
374
1077
  }
375
1078
  catch (err) {
376
- setLoginError(err?.message || "Unable to login");
1079
+ setLoginError(err?.message || 'Unable to login');
377
1080
  }
378
1081
  finally {
379
1082
  setIsLoggingIn(false);
@@ -399,7 +1102,7 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
399
1102
  }
400
1103
  }
401
1104
  setAuth(null);
402
- setLoginPassword("");
1105
+ setLoginPassword('');
403
1106
  clearShareInfo();
404
1107
  setShowLogin(options?.showLogin ?? false);
405
1108
  }
@@ -408,7 +1111,7 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
408
1111
  }
409
1112
  }
410
1113
  function handleUnauthorized() {
411
- void logout({ showLogin: true });
1114
+ void logout({ showLogin: !isLiveMode });
412
1115
  }
413
1116
  const handleUnauthorizedStatus = (status) => {
414
1117
  if (status === 401) {
@@ -426,11 +1129,12 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
426
1129
  return () => {
427
1130
  clearCopyStatusTimer();
428
1131
  clearSessionExpiryTimer();
1132
+ clearLiveChildCloseTimer();
429
1133
  };
430
1134
  }, []);
431
1135
  useEffect(() => {
432
1136
  const pressed = shortcutKeysRef.current;
433
- if (typeof window === "undefined")
1137
+ if (typeof window === 'undefined')
434
1138
  return;
435
1139
  const handleKeyDown = (evt) => {
436
1140
  const hasMod = evt.ctrlKey || evt.metaKey;
@@ -438,10 +1142,10 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
438
1142
  pressed.clear();
439
1143
  return;
440
1144
  }
441
- const key = (evt.key || "").toLowerCase();
442
- if (key === "r" || key === "o") {
1145
+ const key = (evt.key || '').toLowerCase();
1146
+ if (key === 'r' || key === 'o') {
443
1147
  pressed.add(key);
444
- const combo = pressed.has("r") && pressed.has("o");
1148
+ const combo = pressed.has('r') && pressed.has('o');
445
1149
  if (combo) {
446
1150
  if (controlsHidden) {
447
1151
  evt.preventDefault();
@@ -456,26 +1160,26 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
456
1160
  }
457
1161
  return;
458
1162
  }
459
- if (key === "control" || key === "meta") {
1163
+ if (key === 'control' || key === 'meta') {
460
1164
  pressed.clear();
461
1165
  return;
462
1166
  }
463
1167
  pressed.clear();
464
1168
  };
465
1169
  const handleKeyUp = (evt) => {
466
- const key = (evt.key || "").toLowerCase();
467
- if (key === "r" || key === "o") {
1170
+ const key = (evt.key || '').toLowerCase();
1171
+ if (key === 'r' || key === 'o') {
468
1172
  pressed.delete(key);
469
1173
  }
470
- else if (key === "control" || key === "meta") {
1174
+ else if (key === 'control' || key === 'meta') {
471
1175
  pressed.clear();
472
1176
  }
473
1177
  };
474
- window.addEventListener("keydown", handleKeyDown, true);
475
- window.addEventListener("keyup", handleKeyUp, true);
1178
+ window.addEventListener('keydown', handleKeyDown, true);
1179
+ window.addEventListener('keyup', handleKeyUp, true);
476
1180
  return () => {
477
- window.removeEventListener("keydown", handleKeyDown, true);
478
- window.removeEventListener("keyup", handleKeyUp, true);
1181
+ window.removeEventListener('keydown', handleKeyDown, true);
1182
+ window.removeEventListener('keyup', handleKeyUp, true);
479
1183
  };
480
1184
  }, [controlsHidden]);
481
1185
  // ---- bootstrap once ----
@@ -483,7 +1187,7 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
483
1187
  let mounted = true;
484
1188
  (async () => {
485
1189
  try {
486
- const resp = await getJSON(`${base}/v1/sdk/bootstrap?appId=${encodeURIComponent(appId)}`, addTenantHeader({ [INTERNAL_HEADER]: "1" }));
1190
+ const resp = await getJSON(`${base}/v1/sdk/bootstrap?appId=${encodeURIComponent(appId)}`, addTenantHeader({ [INTERNAL_HEADER]: '1' }));
487
1191
  if (mounted && resp.enabled && resp.sdkToken) {
488
1192
  sdkTokenRef.current = resp.sdkToken;
489
1193
  setReady(true);
@@ -502,20 +1206,174 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
502
1206
  }, [appId, base]);
503
1207
  const rrBufferRef = useRef([]);
504
1208
  const rrFlushTimerRef = useRef(null);
1209
+ const rrFlushQueuedRef = useRef(false);
505
1210
  const CHUNK_SIZE = 80; // send when buffer hits 200 events
506
1211
  const FLUSH_MS = 1500; // or every 2s, whichever first
507
- async function sendChunkGzip({ baseUrl, sid, token, envelope, seq }) {
1212
+ const postCanonicalEventsNow = async (events) => {
1213
+ try {
1214
+ const fetcher = origFetchRef.current ?? window.fetch.bind(window);
1215
+ const headers = {
1216
+ 'Content-Type': 'application/json',
1217
+ 'X-App-Id': appId,
1218
+ [INTERNAL_HEADER]: '1',
1219
+ };
1220
+ if (ingest?.appSecret) {
1221
+ headers['X-App-Secret'] = ingest.appSecret;
1222
+ }
1223
+ if (ingest?.appName) {
1224
+ headers['X-App-Name'] = ingest.appName;
1225
+ }
1226
+ const resp = await fetcher(ingestUrl, {
1227
+ method: 'POST',
1228
+ headers: addTenantHeader(headers),
1229
+ body: JSON.stringify({ events }),
1230
+ });
1231
+ if (resp.status === 413) {
1232
+ return 'too_large';
1233
+ }
1234
+ if (!resp.ok) {
1235
+ return 'fail';
1236
+ }
1237
+ return 'ok';
1238
+ }
1239
+ catch {
1240
+ return 'fail';
1241
+ }
1242
+ };
1243
+ const postFrontendActionEvent = (sid, action) => {
1244
+ const safeStart = Number.isFinite(Number(action.tStart))
1245
+ ? Number(action.tStart)
1246
+ : nowServer();
1247
+ const safeEnd = Number.isFinite(Number(action.tEnd))
1248
+ ? Number(action.tEnd)
1249
+ : safeStart;
1250
+ const payload = {
1251
+ action_id: action.aid,
1252
+ aid: action.aid,
1253
+ t: safeStart,
1254
+ label: action.label ?? null,
1255
+ tStart: safeStart,
1256
+ tEnd: safeEnd,
1257
+ hasReq: Boolean(action.hasReq),
1258
+ hasDb: Boolean(action.hasDb),
1259
+ error: Boolean(action.error),
1260
+ ui: action.ui ?? {},
1261
+ };
1262
+ const canonical = {
1263
+ schema_version: ingest?.schemaVersion ?? 1,
1264
+ tenant_id: resolvedTenantId,
1265
+ app_id: appId,
1266
+ session_id: sid,
1267
+ event_type: 'frontend_action',
1268
+ event_ts: new Date(safeEnd).toISOString(),
1269
+ actor_id: ingest?.actorId ?? appId,
1270
+ actor_type: ingest?.actorType ?? 'browser',
1271
+ payload,
1272
+ };
1273
+ if (actorLabels) {
1274
+ canonical.actor_labels = actorLabels;
1275
+ }
1276
+ enqueueCanonicalEvents([canonical]);
1277
+ };
1278
+ const postSessionEndEvent = (sid, payload) => {
1279
+ const finishedAt = Number.isFinite(Number(payload.finishedAt))
1280
+ ? Number(payload.finishedAt)
1281
+ : nowServer();
1282
+ const canonical = {
1283
+ schema_version: ingest?.schemaVersion ?? 1,
1284
+ tenant_id: resolvedTenantId,
1285
+ app_id: appId,
1286
+ session_id: sid,
1287
+ event_type: 'frontend_session_end',
1288
+ event_ts: new Date(finishedAt).toISOString(),
1289
+ actor_id: ingest?.actorId ?? appId,
1290
+ actor_type: ingest?.actorType ?? 'browser',
1291
+ payload: {
1292
+ finishedAt,
1293
+ rrwebNextSeq: Number.isFinite(Number(payload.rrwebNextSeq))
1294
+ ? Number(payload.rrwebNextSeq)
1295
+ : nextSeqRef.current,
1296
+ },
1297
+ };
1298
+ if (actorLabels) {
1299
+ canonical.actor_labels = actorLabels;
1300
+ }
1301
+ enqueueCanonicalEvents([canonical]);
1302
+ };
1303
+ const postSessionCaptureManifest = (sid, payload) => {
1304
+ const closedAt = Number.isFinite(Number(payload.closedAt))
1305
+ ? Number(payload.closedAt)
1306
+ : nowServer();
1307
+ const frontendActionDocCount = Number.isFinite(Number(payload.frontendActionDocCount))
1308
+ ? Math.max(0, Math.round(Number(payload.frontendActionDocCount)))
1309
+ : 0;
1310
+ const rrwebChunkCount = Number.isFinite(Number(payload.rrwebChunkCount))
1311
+ ? Math.max(0, Math.round(Number(payload.rrwebChunkCount)))
1312
+ : 0;
1313
+ const canonical = {
1314
+ schema_version: ingest?.schemaVersion ?? 1,
1315
+ tenant_id: resolvedTenantId,
1316
+ app_id: appId,
1317
+ session_id: sid,
1318
+ event_type: 'session_capture_manifest',
1319
+ event_ts: new Date(closedAt).toISOString(),
1320
+ actor_id: ingest?.actorId ?? appId,
1321
+ actor_type: ingest?.actorType ?? 'browser',
1322
+ payload: {
1323
+ mode: 'staging',
1324
+ closedAt,
1325
+ expected: {
1326
+ frontendActionDoc: frontendActionDocCount,
1327
+ rrwebChunk: rrwebChunkCount,
1328
+ frontendSessionEnd: 1,
1329
+ },
1330
+ },
1331
+ };
1332
+ if (actorLabels) {
1333
+ canonical.actor_labels = actorLabels;
1334
+ }
1335
+ enqueueCanonicalEvents([canonical]);
1336
+ };
1337
+ const sendChunkToLogCollector = async ({ sid, envelope, seq, }) => {
1338
+ const tFirst = Number(envelope?.tFirst);
1339
+ const tLast = Number(envelope?.tLast);
1340
+ const safeTFirst = Number.isFinite(tFirst) ? tFirst : nowServer();
1341
+ const safeTLast = Number.isFinite(tLast) ? tLast : safeTFirst;
1342
+ const payload = {
1343
+ action_id: currentAidRef.current ?? null,
1344
+ seq,
1345
+ tFirst: safeTFirst,
1346
+ tLast: safeTLast,
1347
+ events: Array.isArray(envelope?.events) ? envelope.events : [],
1348
+ };
1349
+ const canonical = {
1350
+ schema_version: ingest?.schemaVersion ?? 1,
1351
+ tenant_id: resolvedTenantId,
1352
+ app_id: appId,
1353
+ session_id: sid,
1354
+ event_type: 'rrweb_chunk',
1355
+ event_ts: new Date(safeTLast).toISOString(),
1356
+ actor_id: ingest?.actorId ?? appId,
1357
+ actor_type: ingest?.actorType ?? 'browser',
1358
+ payload,
1359
+ };
1360
+ if (actorLabels) {
1361
+ canonical.actor_labels = actorLabels;
1362
+ }
1363
+ return postCanonicalEventsNow([canonical]);
1364
+ };
1365
+ async function sendChunkGzip({ baseUrl, sid, token, envelope, seq, }) {
508
1366
  try {
509
1367
  const json = JSON.stringify({ ...envelope, seq });
510
1368
  const gz = gzip(json); // Uint8Array
511
1369
  const r = await (origFetchRef.current ?? window.fetch)(`${baseUrl}/v1/sessions/${sid}/events`, {
512
- method: "POST",
1370
+ method: 'POST',
513
1371
  headers: addTenantHeader({
514
- "Content-Type": "application/json",
515
- "Content-Encoding": "gzip",
516
- "x-sdk-token": token,
1372
+ 'Content-Type': 'application/json',
1373
+ 'Content-Encoding': 'gzip',
1374
+ 'x-sdk-token': token,
517
1375
  ...(userTokenRef.current ? { Authorization: `Bearer ${userTokenRef.current}` } : {}),
518
- [INTERNAL_HEADER]: "1",
1376
+ [INTERNAL_HEADER]: '1',
519
1377
  }),
520
1378
  body: gz,
521
1379
  });
@@ -531,20 +1389,40 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
531
1389
  return 'fail';
532
1390
  }
533
1391
  }
1392
+ const enqueueRrwebBufferFlush = (reason) => {
1393
+ if (rrFlushQueuedRef.current)
1394
+ return;
1395
+ rrFlushQueuedRef.current = true;
1396
+ enqueueSdkTask({
1397
+ maxAttempts: 5,
1398
+ run: async () => {
1399
+ rrFlushQueuedRef.current = false;
1400
+ const ok = await flushRrwebBufferNow(reason);
1401
+ if (ok && rrBufferRef.current.length > 0) {
1402
+ enqueueRrwebBufferFlush('timer');
1403
+ }
1404
+ return ok;
1405
+ },
1406
+ });
1407
+ };
534
1408
  async function flushRrwebBuffer(reason) {
1409
+ enqueueRrwebBufferFlush(reason);
1410
+ }
1411
+ async function flushRrwebBufferNow(reason) {
535
1412
  if (isFlushingRef.current)
536
- return;
1413
+ return false;
537
1414
  const sid = sessionIdRef.current;
538
1415
  const token = sdkTokenRef.current;
539
1416
  const baseUrl = base;
540
1417
  if (!sid || !token)
541
- return;
1418
+ return true;
542
1419
  if (!rrBufferRef.current.length)
543
- return;
1420
+ return true;
544
1421
  isFlushingRef.current = true;
1422
+ let failed = false;
545
1423
  try {
546
1424
  if (backoffRef.current > 0) {
547
- await new Promise(r => setTimeout(r, backoffRef.current));
1425
+ await new Promise((r) => setTimeout(r, backoffRef.current));
548
1426
  }
549
1427
  // COPY buffer; do not mutate until we know what succeeded
550
1428
  const fullSlice = rrBufferRef.current.slice(0);
@@ -564,7 +1442,10 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
564
1442
  const env = mkEnvelope(piece);
565
1443
  const jsonSize = jsonBytes(env);
566
1444
  let res;
567
- if (jsonSize > 64 * 1024) {
1445
+ if (useIngestPipeline) {
1446
+ res = await sendChunkToLogCollector({ sid, envelope: env, seq: pieceSeq });
1447
+ }
1448
+ else if (jsonSize > 64 * 1024) {
568
1449
  res = await sendChunkGzip({ baseUrl, sid, token, envelope: env, seq: pieceSeq });
569
1450
  }
570
1451
  else {
@@ -586,26 +1467,28 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
586
1467
  }
587
1468
  // failure: stop; keep remaining events in buffer for retry
588
1469
  backoffRef.current = Math.min(4000, (backoffRef.current || 250) * 2);
1470
+ failed = true;
589
1471
  break;
590
1472
  }
591
1473
  // success for `sentCount` events → drop them from buffer
592
1474
  if (sentCount > 0) {
593
1475
  rrBufferRef.current.splice(0, sentCount);
594
1476
  }
1477
+ return !failed;
595
1478
  }
596
1479
  finally {
597
1480
  isFlushingRef.current = false;
598
1481
  }
599
1482
  }
600
- async function sendChunk({ baseUrl, sid, token, envelope, seq }) {
1483
+ async function sendChunk({ baseUrl, sid, token, envelope, seq, }) {
601
1484
  try {
602
1485
  const r = await (origFetchRef.current ?? window.fetch)(`${baseUrl}/v1/sessions/${sid}/events`, {
603
- method: "POST",
1486
+ method: 'POST',
604
1487
  headers: addTenantHeader({
605
- "Content-Type": "application/json",
606
- "x-sdk-token": token,
1488
+ 'Content-Type': 'application/json',
1489
+ 'x-sdk-token': token,
607
1490
  ...(userTokenRef.current ? { Authorization: `Bearer ${userTokenRef.current}` } : {}),
608
- [INTERNAL_HEADER]: "1",
1491
+ [INTERNAL_HEADER]: '1',
609
1492
  }),
610
1493
  body: JSON.stringify({ ...envelope, seq }), // ensure seq matches piece
611
1494
  });
@@ -631,343 +1514,438 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
631
1514
  return;
632
1515
  ax.__reproAttached = true;
633
1516
  ax.interceptors.request.use((config) => {
634
- const sid = sessionIdRef.current;
635
- const aid = currentAidRef.current;
636
- const url = `${config.baseURL || ""}${config.url || ""}`;
1517
+ const url = `${config.baseURL || ''}${config.url || ''}`;
637
1518
  const isInternal = url.startsWith(base);
638
1519
  const isSdkInternal = config.headers?.[INTERNAL_HEADER] != null;
1520
+ const isClientRequest = !isInternal && !isSdkInternal;
1521
+ const existingHeaders = new Headers(config.headers || {});
1522
+ const resolved = resolveOutgoingRequestContext({
1523
+ url,
1524
+ isInternal,
1525
+ isSdkInternal,
1526
+ method: config.method,
1527
+ });
1528
+ const sid = resolved.sessionId;
1529
+ const aid = resolved.actionId;
639
1530
  if (isInternal) {
640
1531
  config.headers = config.headers || {};
641
1532
  config.headers[NGROK_SKIP_HEADER] = NGROK_SKIP_VALUE;
642
1533
  }
643
- if (sid && aid && !isInternal && !isSdkInternal) {
644
- config.headers = config.headers || {};
645
- config.headers["X-Bug-Session-Id"] = sid;
646
- config.headers["X-Bug-Action-Id"] = aid;
1534
+ config.headers = config.headers || {};
1535
+ const useQueryPropagation = sid && aid
1536
+ ? shouldUseQueryPropagation({
1537
+ url,
1538
+ method: config.method,
1539
+ headers: existingHeaders,
1540
+ isInternal,
1541
+ isSdkInternal,
1542
+ })
1543
+ : false;
1544
+ if (sid && aid && !isSdkInternal && !useQueryPropagation) {
1545
+ config.headers[REQUEST_START_HEADER] = String(resolved.requestStart);
1546
+ }
1547
+ if (sid && aid && !isInternal && !isSdkInternal && useQueryPropagation) {
1548
+ config.url = appendReproQuery(url, {
1549
+ sessionId: sid,
1550
+ actionId: aid,
1551
+ requestStart: resolved.requestStart,
1552
+ });
1553
+ if (config.baseURL) {
1554
+ config.baseURL = undefined;
1555
+ }
1556
+ }
1557
+ else if (sid && aid && !isInternal && !isSdkInternal) {
1558
+ config.headers['X-Bug-Session-Id'] = sid;
1559
+ config.headers['X-Bug-Action-Id'] = aid;
1560
+ }
1561
+ config.__reproSessionId = sid ?? null;
1562
+ config.__reproActionId = aid ?? null;
1563
+ config.__reproActionLabel = resolved.label ?? null;
1564
+ if (isClientRequest) {
1565
+ beginClientRequest();
1566
+ config.__reproClientRequest = true;
647
1567
  }
648
1568
  return config;
649
1569
  });
650
- ax.interceptors.response.use(async (resp) => {
651
- const url = `${resp.config.baseURL || ""}${resp.config.url || ""}`;
1570
+ ax.interceptors.response.use((resp) => {
1571
+ const url = `${resp.config.baseURL || ''}${resp.config.url || ''}`;
652
1572
  const hdrs = resp.config.headers || {};
653
1573
  const isInternal = url.startsWith(base);
654
1574
  const isSdkInternal = hdrs[INTERNAL_HEADER] != null;
655
1575
  if (isInternal) {
656
1576
  handleUnauthorizedStatus(resp.status);
657
1577
  }
658
- if (!isInternal &&
659
- !isSdkInternal &&
660
- sessionIdRef.current &&
661
- currentAidRef.current &&
662
- sdkTokenRef.current &&
663
- !hasReqMarkedRef.current.has(currentAidRef.current)) {
664
- hasReqMarkedRef.current.add(currentAidRef.current);
665
- // use ORIGINAL fetch to avoid recursion
666
- await origFetchRef.current(`${base}/v1/sessions/${sessionIdRef.current}/events`, {
667
- method: "POST",
668
- headers: addTenantHeader({
669
- "Content-Type": "application/json",
670
- "x-sdk-token": sdkTokenRef.current,
671
- ...(userTokenRef.current ? { Authorization: `Bearer ${userTokenRef.current}` } : {}),
672
- [INTERNAL_HEADER]: "1",
673
- }),
674
- body: JSON.stringify({
675
- seq: nowServer(),
676
- events: [
677
- {
678
- type: "action",
679
- aid: currentAidRef.current,
680
- label: lastActionLabelRef.current,
681
- tStart: nowServer(),
682
- tEnd: nowServer(),
683
- hasReq: true,
684
- ui: {},
685
- },
686
- ],
687
- }),
688
- }).catch(() => { });
1578
+ const sid = resp?.config?.__reproSessionId ?? null;
1579
+ const aid = resp?.config?.__reproActionId ?? null;
1580
+ const label = resp?.config?.__reproActionLabel ?? null;
1581
+ if (!isInternal && !isSdkInternal && sid && aid) {
1582
+ notifyRequestCompleted({
1583
+ sessionId: sid,
1584
+ actionId: aid,
1585
+ label,
1586
+ finishedAt: nowServer(),
1587
+ });
1588
+ }
1589
+ if (resp?.config?.__reproClientRequest) {
1590
+ endClientRequest();
689
1591
  }
690
1592
  return resp;
691
- }, (err) => Promise.reject(err));
1593
+ }, (err) => {
1594
+ const requestConfig = err?.config ?? err?.response?.config;
1595
+ if (requestConfig?.__reproClientRequest) {
1596
+ endClientRequest();
1597
+ }
1598
+ return Promise.reject(err);
1599
+ });
692
1600
  }
693
1601
  // ---- derive a label from a click target (best-effort, minimal) ----
694
1602
  function labelFromClickTarget(target) {
695
1603
  const el = target;
696
1604
  if (!el)
697
- return "Click";
698
- const txt = (el.innerText || el.getAttribute?.("aria-label") || "").trim().slice(0, 40) ||
699
- el.getAttribute?.("title") ||
700
- "";
701
- const id = el.id ? `#${el.id}` : "";
702
- const cls = el.className && typeof el.className === "string"
703
- ? `.${el.className.split(" ").slice(0, 2).join(".")}`
704
- : "";
705
- return txt ? `Click • ${txt}` : `Click • ${(el.tagName || "el").toLowerCase()}${id || cls}`;
1605
+ return 'Click';
1606
+ const txt = (el.innerText || el.getAttribute?.('aria-label') || '').trim().slice(0, 40) ||
1607
+ el.getAttribute?.('title') ||
1608
+ '';
1609
+ const id = el.id ? `#${el.id}` : '';
1610
+ const cls = el.className && typeof el.className === 'string'
1611
+ ? `.${el.className.split(' ').slice(0, 2).join('.')}`
1612
+ : '';
1613
+ return txt ? `Click • ${txt}` : `Click • ${(el.tagName || 'el').toLowerCase()}${id || cls}`;
706
1614
  }
707
1615
  // ---- START recording ----
708
1616
  async function start() {
709
1617
  if (!sdkTokenRef.current || recordingRef.current)
710
1618
  return;
711
- // 1) start session
712
- if (!requireAuth())
1619
+ if (!isLiveMode && !requireAuth())
713
1620
  return;
714
1621
  clearShareInfo();
715
- const r = await fetch(`${base}/v1/sessions`, {
716
- method: "POST",
717
- headers: addTenantHeader({
718
- "Content-Type": "application/json",
719
- "x-sdk-token": sdkTokenRef.current,
720
- ...(userTokenRef.current ? { Authorization: `Bearer ${userTokenRef.current}` } : {}),
721
- }),
722
- body: JSON.stringify({ clientTime: nowServer() }),
723
- });
724
- if (!r.ok) {
725
- handleUnauthorizedStatus(r.status);
726
- return;
727
- }
728
- const sess = (await r.json());
729
- sessionIdRef.current = sess.sessionId; // <-- MISSING, add this
730
- offsetRef.current = Number(sess.clockOffsetMs || 0);
731
- // 2) hold original fetch & install interceptor
732
1622
  origFetchRef.current = window.fetch.bind(window);
733
- nextSeqRef.current = 1; // reset for new session
1623
+ nextSeqRef.current = 1;
734
1624
  rrBufferRef.current = [];
1625
+ liveRecentEventsRef.current = [];
1626
+ liveLatestMetaEventRef.current = null;
1627
+ liveSinceFullSnapshotRef.current = [];
1628
+ activeChildSessionRef.current = null;
1629
+ parentSessionRef.current = null;
1630
+ previousChildSessionIdRef.current = null;
1631
+ liveParentRotationPendingRef.current = false;
1632
+ hasReqMarkedRef.current.clear();
1633
+ actionMeta.current.clear();
1634
+ clearSessionExpiryTimer();
1635
+ clearLiveChildCloseTimer();
1636
+ if (isLiveMode) {
1637
+ const parent = ensureLiveParentSession();
1638
+ if (!parent) {
1639
+ return;
1640
+ }
1641
+ sessionIdRef.current = parent.sessionId;
1642
+ }
1643
+ else {
1644
+ const sessionId = newSID();
1645
+ sessionIdRef.current = sessionId;
1646
+ enqueueCreateSdkSession({
1647
+ sessionId,
1648
+ clientTime: nowServer(),
1649
+ sessionKind: 'standard',
1650
+ }, (created) => {
1651
+ if (sessionIdRef.current === created.sessionId) {
1652
+ offsetRef.current = Number(created.clockOffsetMs || 0);
1653
+ }
1654
+ });
1655
+ }
735
1656
  if (rrFlushTimerRef.current)
736
1657
  window.clearInterval(rrFlushTimerRef.current);
737
- rrFlushTimerRef.current = window.setInterval(() => {
738
- flushRrwebBuffer('timer');
739
- }, FLUSH_MS);
1658
+ if (!isLiveMode) {
1659
+ rrFlushTimerRef.current = window.setInterval(() => {
1660
+ void flushRrwebBuffer('timer');
1661
+ }, FLUSH_MS);
1662
+ }
740
1663
  window.fetch = async (input, init = {}) => {
741
- // figure request url
742
- const urlStr = typeof input === "string" || input instanceof URL
743
- ? String(input)
744
- : input.url;
745
- // existing incoming headers (if any)
1664
+ const urlStr = typeof input === 'string' || input instanceof URL ? String(input) : input.url;
746
1665
  const hdrsIn = new Headers(init.headers || input?.headers || {});
747
1666
  const isInternal = urlStr.startsWith(base);
748
1667
  const isSdkInternal = hdrsIn.has(INTERNAL_HEADER) || hdrsIn.has(INTERNAL_HEADER.toLowerCase());
749
- // inject bug headers for app requests only (not our API or SDK-internal posts)
750
- const headers = new Headers(init.headers || {});
1668
+ const isClientRequest = !isInternal && !isSdkInternal;
1669
+ const method = init.method ??
1670
+ (typeof Request !== 'undefined' && input instanceof Request ? input.method : undefined);
1671
+ const resolved = resolveOutgoingRequestContext({
1672
+ url: urlStr,
1673
+ isInternal,
1674
+ isSdkInternal,
1675
+ method,
1676
+ });
1677
+ const headers = new Headers(init.headers || input?.headers || {});
1678
+ const useQueryPropagation = resolved.sessionId && resolved.actionId
1679
+ ? shouldUseQueryPropagation({
1680
+ url: urlStr,
1681
+ method,
1682
+ headers,
1683
+ isInternal,
1684
+ isSdkInternal,
1685
+ })
1686
+ : false;
751
1687
  setTenantOnHeaders(headers, isInternal);
752
- if (!isSdkInternal) {
753
- const requestStart = nowServer();
754
- headers.set(REQUEST_START_HEADER, String(requestStart));
1688
+ if (resolved.sessionId && resolved.actionId && !isSdkInternal && !useQueryPropagation) {
1689
+ headers.set(REQUEST_START_HEADER, String(resolved.requestStart));
755
1690
  }
756
- if (sessionIdRef.current && currentAidRef.current && !isInternal && !isSdkInternal) {
757
- headers.set("X-Bug-Session-Id", sessionIdRef.current);
758
- headers.set("X-Bug-Action-Id", currentAidRef.current);
1691
+ let nextInput = input;
1692
+ if (resolved.sessionId && resolved.actionId && !isInternal && !isSdkInternal && useQueryPropagation) {
1693
+ const nextUrl = appendReproQuery(urlStr, {
1694
+ sessionId: resolved.sessionId,
1695
+ actionId: resolved.actionId,
1696
+ requestStart: resolved.requestStart,
1697
+ });
1698
+ if (typeof input === 'string') {
1699
+ nextInput = nextUrl;
1700
+ }
1701
+ else if (input instanceof URL) {
1702
+ nextInput = new URL(nextUrl);
1703
+ }
1704
+ else if (typeof Request !== 'undefined' && input instanceof Request) {
1705
+ nextInput = new Request(nextUrl, input);
1706
+ }
759
1707
  }
760
- init.headers = headers;
761
- // always call ORIGINAL fetch to avoid recursion
762
- const res = await origFetchRef.current(input, init);
763
- if (isInternal) {
764
- handleUnauthorizedStatus(res.status);
1708
+ else if (resolved.sessionId && resolved.actionId && !isInternal && !isSdkInternal) {
1709
+ headers.set('X-Bug-Session-Id', resolved.sessionId);
1710
+ headers.set('X-Bug-Action-Id', resolved.actionId);
1711
+ }
1712
+ const nextInit = {
1713
+ ...init,
1714
+ headers,
1715
+ };
1716
+ if (isClientRequest) {
1717
+ beginClientRequest();
1718
+ }
1719
+ try {
1720
+ const res = await origFetchRef.current(nextInput, nextInit);
1721
+ if (isInternal) {
1722
+ handleUnauthorizedStatus(res.status);
1723
+ }
1724
+ if (!isInternal && !isSdkInternal && resolved.sessionId && resolved.actionId) {
1725
+ notifyRequestCompleted({
1726
+ sessionId: resolved.sessionId,
1727
+ actionId: resolved.actionId,
1728
+ label: resolved.label,
1729
+ finishedAt: nowServer(),
1730
+ });
1731
+ }
1732
+ return res;
1733
+ }
1734
+ catch (err) {
1735
+ if (isLiveMode) {
1736
+ const child = activeChildSessionRef.current;
1737
+ if (child && child.sessionId === resolved.sessionId) {
1738
+ child.requestsInFlight = Math.max(0, child.requestsInFlight - 1);
1739
+ child.closeDeadline = Math.max(child.closeDeadline, resolved.requestStart + LIVE_REQUEST_POSTROLL_MS + LIVE_SEGMENT_IDLE_MS);
1740
+ scheduleLiveChildCloseCheck();
1741
+ }
1742
+ }
1743
+ throw err;
765
1744
  }
766
- // mark hasReq ONCE per action (skip internal/API calls)
767
- if (!isInternal &&
768
- !isSdkInternal &&
769
- sessionIdRef.current &&
770
- currentAidRef.current &&
771
- !hasReqMarkedRef.current.has(currentAidRef.current)) {
772
- hasReqMarkedRef.current.add(currentAidRef.current);
1745
+ finally {
1746
+ if (isClientRequest) {
1747
+ endClientRequest();
1748
+ }
773
1749
  }
774
- return res;
775
1750
  };
776
1751
  attachAxiosIfPresent();
777
- // 3) click -> new ActionId + minimal action event (via ORIGINAL fetch)
778
- // Remove any previous handler to avoid duplicates
779
1752
  if (clickHandlerRef.current) {
780
- document.removeEventListener("click", clickHandlerRef.current, { capture: true });
1753
+ document.removeEventListener('click', clickHandlerRef.current, { capture: true });
781
1754
  clickHandlerRef.current = null;
782
1755
  }
783
1756
  const clickHandler = (evt) => {
784
1757
  if (!sessionIdRef.current || !sdkTokenRef.current)
785
1758
  return;
786
- // Skip clicks on the SDK's own UI
787
1759
  const targetEl = evt.target;
788
1760
  if (targetEl && targetEl.closest('[data-repro-internal="1"]'))
789
1761
  return;
790
- // Dedupe: ignore same-label clicks within 250ms (dev/StrictMode safety)
791
1762
  const label = labelFromClickTarget(targetEl);
792
1763
  const t = nowServer();
793
- if (lastClickRef.current && t - lastClickRef.current.t < 250 && lastClickRef.current.label === label) {
1764
+ if (lastClickRef.current &&
1765
+ t - lastClickRef.current.t < 250 &&
1766
+ lastClickRef.current.label === label) {
794
1767
  return;
795
1768
  }
796
1769
  lastClickRef.current = { t, label };
797
1770
  const aid = newAID();
798
1771
  currentAidRef.current = aid;
799
1772
  lastActionLabelRef.current = label;
1773
+ actionMeta.current.set(aid, { tStart: t, label });
800
1774
  if (aidExpiryTimerRef.current)
801
1775
  window.clearTimeout(aidExpiryTimerRef.current);
802
1776
  aidExpiryTimerRef.current = window.setTimeout(() => {
803
1777
  currentAidRef.current = null;
804
1778
  }, 5000);
805
- // post action row (internal; avoid recursion)
806
- origFetchRef.current(`${base}/v1/sessions/${sessionIdRef.current}/events`, {
807
- method: "POST",
808
- headers: addTenantHeader({
809
- "Content-Type": "application/json",
810
- "x-sdk-token": sdkTokenRef.current,
811
- ...(userTokenRef.current ? { Authorization: `Bearer ${userTokenRef.current}` } : {}),
812
- [INTERNAL_HEADER]: "1",
813
- }),
814
- body: JSON.stringify({
815
- seq: t,
816
- events: [
817
- {
818
- type: "action",
819
- aid,
820
- label,
821
- tStart: t,
822
- tEnd: t,
823
- hasReq: false,
824
- hasDb: false,
825
- error: false,
826
- ui: { kind: "click" },
827
- },
828
- ],
829
- }),
830
- })
831
- .then((resp) => {
832
- handleUnauthorizedStatus(resp.status);
833
- })
834
- .catch(() => { });
1779
+ if (!isLiveMode) {
1780
+ postFrontendActionEvent(sessionIdRef.current, {
1781
+ aid,
1782
+ label,
1783
+ tStart: t,
1784
+ tEnd: t,
1785
+ hasReq: false,
1786
+ hasDb: false,
1787
+ error: false,
1788
+ ui: { kind: 'click' },
1789
+ });
1790
+ }
835
1791
  };
836
1792
  clickHandlerRef.current = clickHandler;
837
- document.addEventListener("click", clickHandler, { capture: true });
838
- // 4) rrweb: keep events in memory; upload on stop()
1793
+ document.addEventListener('click', clickHandler, { capture: true });
839
1794
  stopRecRef.current = record({
840
1795
  emit: (ev) => {
1796
+ if (isLiveMode) {
1797
+ if (Number(ev?.type) === EventType.Meta) {
1798
+ liveLatestMetaEventRef.current = ev;
1799
+ }
1800
+ if (Number(ev?.type) === EventType.FullSnapshot) {
1801
+ liveSinceFullSnapshotRef.current = [ev];
1802
+ }
1803
+ else if (liveSinceFullSnapshotRef.current.length > 0) {
1804
+ liveSinceFullSnapshotRef.current.push(ev);
1805
+ }
1806
+ liveRecentEventsRef.current.push(ev);
1807
+ trimLiveRecentEvents();
1808
+ const child = activeChildSessionRef.current;
1809
+ if (child) {
1810
+ child.events.push(ev);
1811
+ }
1812
+ return;
1813
+ }
841
1814
  rrBufferRef.current.push(ev);
842
- // stream out if we reach CHUNK_SIZE
843
1815
  if (rrBufferRef.current.length >= CHUNK_SIZE) {
844
- flushRrwebBuffer('size');
1816
+ void flushRrwebBuffer('size');
845
1817
  }
846
1818
  },
1819
+ ...(isLiveMode ? { checkoutEveryNms: LIVE_RRWEB_CHECKOUT_MS } : {}),
847
1820
  ...(masking ?? {}),
848
1821
  });
849
- // 5) cleanup registration
850
1822
  stop._cleanup = () => {
851
1823
  if (clickHandlerRef.current) {
852
- document.removeEventListener("click", clickHandlerRef.current, { capture: true });
1824
+ document.removeEventListener('click', clickHandlerRef.current, { capture: true });
853
1825
  clickHandlerRef.current = null;
854
1826
  }
855
1827
  };
856
- clearSessionExpiryTimer();
857
- sessionExpiryTimerRef.current = window.setTimeout(() => {
858
- void stop();
859
- }, SESSION_MAX_MS);
1828
+ if (!isLiveMode) {
1829
+ clearSessionExpiryTimer();
1830
+ sessionExpiryTimerRef.current = window.setTimeout(() => {
1831
+ void stop();
1832
+ }, SESSION_MAX_MS);
1833
+ }
860
1834
  recordingRef.current = true;
861
1835
  setRecording(true);
862
1836
  }
863
1837
  // ---- STOP recording ----
864
1838
  async function stop() {
865
1839
  clearSessionExpiryTimer();
1840
+ clearLiveChildCloseTimer();
866
1841
  if (!recordingRef.current || isStoppingRef.current)
867
1842
  return;
868
1843
  isStoppingRef.current = true;
869
1844
  try {
870
- // stop rrweb + listeners
871
1845
  stopRecRef.current?.();
872
1846
  stop._cleanup?.();
873
- // clear periodic timer
874
1847
  if (rrFlushTimerRef.current) {
875
1848
  window.clearInterval(rrFlushTimerRef.current);
876
1849
  rrFlushTimerRef.current = null;
877
1850
  }
878
- // final flush of whatever is left
879
- await flushRrwebBuffer('stop');
880
- const origFetch = origFetchRef.current ?? window.fetch.bind(window);
881
- const sid = sessionIdRef.current;
882
- const token = sdkTokenRef.current;
883
- // 1) upload rrweb chunks (internal; use original fetch)
884
- try {
885
- const events = rrwebEventsRef.current;
886
- const CHUNK = 500;
887
- for (let i = 0; i < events.length; i += CHUNK) {
888
- const slice = events.slice(i, i + CHUNK);
889
- const seq = nextSeqRef.current++;
890
- const tFirst = slice[0]?.timestamp ?? nowServer();
891
- const tLast = slice[slice.length - 1]?.timestamp ?? tFirst;
892
- const chunkRes = await origFetch(`${base}/v1/sessions/${sid}/events`, {
893
- method: "POST",
894
- headers: addTenantHeader({
895
- "Content-Type": "application/json",
896
- "x-sdk-token": token,
897
- ...(userTokenRef.current ? { Authorization: `Bearer ${userTokenRef.current}` } : {}),
898
- [INTERNAL_HEADER]: "1",
899
- }),
900
- body: JSON.stringify({
901
- type: "rrweb",
902
- seq,
903
- tFirst,
904
- tLast,
905
- events: slice, // send raw array
906
- }),
907
- });
908
- if (handleUnauthorizedStatus(chunkRes.status)) {
909
- break;
910
- }
1851
+ if (isLiveMode) {
1852
+ const child = activeChildSessionRef.current;
1853
+ if (child) {
1854
+ child.requestsInFlight = 0;
1855
+ child.closeDeadline = nowServer();
1856
+ await finalizeLiveChildSession();
911
1857
  }
1858
+ const parent = parentSessionRef.current;
1859
+ parentSessionRef.current = null;
1860
+ liveParentRotationPendingRef.current = false;
1861
+ if (parent) {
1862
+ enqueueFinishSdkSession(parent.sessionId);
1863
+ }
1864
+ await flushSdkQueue();
1865
+ clearShareInfo();
912
1866
  }
913
- catch {
914
- /* ignore in MVP */
915
- }
916
- finally {
1867
+ else {
1868
+ await flushSdkQueue();
1869
+ const rrwebFlushed = await flushRrwebBufferNow('stop');
1870
+ if (!rrwebFlushed && rrBufferRef.current.length > 0) {
1871
+ enqueueRrwebBufferFlush('stop');
1872
+ await flushSdkQueue();
1873
+ }
1874
+ const origFetch = origFetchRef.current ?? window.fetch.bind(window);
1875
+ const sid = sessionIdRef.current;
1876
+ const token = sdkTokenRef.current;
1877
+ const finishedAt = nowServer();
1878
+ const rrwebNextSeq = nextSeqRef.current;
1879
+ const frontendActionDocCount = actionMeta.current.size;
1880
+ const rrwebChunkCount = Math.max(0, rrwebNextSeq - 1);
917
1881
  rrwebEventsRef.current = [];
918
- nextSeqRef.current = 1;
919
- actionMeta.current.clear();
920
- }
921
- // 2) finish (internal; use original fetch)
922
- try {
923
- const res = await origFetch(`${base}/v1/sessions/${sid}/finish`, {
924
- method: "POST",
925
- headers: addTenantHeader({
926
- "Content-Type": "application/json",
927
- "x-sdk-token": token,
928
- ...(userTokenRef.current ? { Authorization: `Bearer ${userTokenRef.current}` } : {}),
929
- [INTERNAL_HEADER]: "1",
930
- }),
931
- body: JSON.stringify({ notes: "" }),
1882
+ postSessionEndEvent(sid, {
1883
+ finishedAt,
1884
+ rrwebNextSeq,
932
1885
  });
933
- if (handleUnauthorizedStatus(res.status)) {
934
- clearShareInfo();
935
- }
936
- else if (res.ok) {
937
- let fin = null;
938
- try {
939
- fin = await res.json();
940
- }
941
- catch {
942
- fin = null;
1886
+ postSessionCaptureManifest(sid, {
1887
+ closedAt: finishedAt,
1888
+ rrwebChunkCount,
1889
+ frontendActionDocCount,
1890
+ });
1891
+ await flushSdkQueue();
1892
+ actionMeta.current.clear();
1893
+ try {
1894
+ const res = await origFetch(`${base}/v1/sessions/${sid}/finish`, {
1895
+ method: 'POST',
1896
+ headers: addTenantHeader({
1897
+ 'Content-Type': 'application/json',
1898
+ 'x-sdk-token': token,
1899
+ ...(userTokenRef.current ? { Authorization: `Bearer ${userTokenRef.current}` } : {}),
1900
+ [INTERNAL_HEADER]: '1',
1901
+ }),
1902
+ body: JSON.stringify({ notes: '' }),
1903
+ });
1904
+ if (handleUnauthorizedStatus(res.status)) {
1905
+ clearShareInfo();
943
1906
  }
944
- if (fin?.viewerUrl) {
945
- resetCopyFeedback();
946
- setShareUrl(fin.viewerUrl);
1907
+ else if (res.ok) {
1908
+ let fin = null;
1909
+ try {
1910
+ fin = await res.json();
1911
+ }
1912
+ catch {
1913
+ fin = null;
1914
+ }
1915
+ if (fin?.viewerUrl) {
1916
+ resetCopyFeedback();
1917
+ setShareUrl(fin.viewerUrl);
1918
+ }
1919
+ else {
1920
+ clearShareInfo();
1921
+ }
947
1922
  }
948
1923
  else {
949
1924
  clearShareInfo();
950
1925
  }
951
1926
  }
952
- else {
1927
+ catch {
953
1928
  clearShareInfo();
954
1929
  }
955
1930
  }
956
- catch {
957
- clearShareInfo();
958
- }
959
1931
  }
960
1932
  finally {
961
- // 3) restore fetch & reset
962
1933
  if (origFetchRef.current)
963
1934
  window.fetch = origFetchRef.current;
964
1935
  sessionIdRef.current = null;
1936
+ parentSessionRef.current = null;
1937
+ activeChildSessionRef.current = null;
1938
+ previousChildSessionIdRef.current = null;
1939
+ liveRecentEventsRef.current = [];
1940
+ liveLatestMetaEventRef.current = null;
1941
+ liveSinceFullSnapshotRef.current = [];
965
1942
  currentAidRef.current = null;
966
1943
  lastActionLabelRef.current = null;
967
1944
  hasReqMarkedRef.current.clear();
1945
+ actionMeta.current.clear();
1946
+ rrBufferRef.current = [];
968
1947
  if (aidExpiryTimerRef.current)
969
1948
  window.clearTimeout(aidExpiryTimerRef.current);
970
- // also reset dedupe
971
1949
  lastClickRef.current = null;
972
1950
  setRecording(false);
973
1951
  recordingRef.current = false;
@@ -975,16 +1953,16 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
975
1953
  }
976
1954
  }
977
1955
  // ---- UI (floating button) ----
978
- const btnLabel = recording ? (button?.text ?? "Stop & Report") : (button?.text ?? "Record");
979
- const loginLabel = "Authenticate to Record";
1956
+ const btnLabel = recording ? button?.text ?? 'Stop & Report' : button?.text ?? 'Record';
1957
+ const loginLabel = 'Authenticate to Record';
980
1958
  const floatingContainerBaseStyle = {
981
- position: "fixed",
1959
+ position: 'fixed',
982
1960
  zIndex: 2147483647,
983
- display: "flex",
984
- flexDirection: "column",
985
- alignItems: "flex-end",
1961
+ display: 'flex',
1962
+ flexDirection: 'column',
1963
+ alignItems: 'flex-end',
986
1964
  gap: 12,
987
- width: "100%",
1965
+ width: '100%',
988
1966
  maxWidth: 360,
989
1967
  };
990
1968
  const floatingContainerStyle = {
@@ -993,146 +1971,146 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
993
1971
  bottom: 16,
994
1972
  };
995
1973
  const buttonRowStyle = {
996
- display: "flex",
1974
+ display: 'flex',
997
1975
  gap: 10,
998
- justifyContent: "flex-end",
999
- width: "100%",
1976
+ justifyContent: 'flex-end',
1977
+ width: '100%',
1000
1978
  };
1001
1979
  const buttonBaseStyle = {
1002
- display: "inline-flex",
1003
- alignItems: "center",
1004
- justifyContent: "center",
1980
+ display: 'inline-flex',
1981
+ alignItems: 'center',
1982
+ justifyContent: 'center',
1005
1983
  gap: 8,
1006
- padding: "12px 24px",
1984
+ padding: '12px 24px',
1007
1985
  borderRadius: 9999,
1008
- border: "none",
1009
- background: "#f4f5f7",
1010
- cursor: "pointer",
1011
- fontFamily: "ui-sans-serif, system-ui, -apple-system",
1012
- fontSize: "14px",
1986
+ border: 'none',
1987
+ background: '#f4f5f7',
1988
+ cursor: 'pointer',
1989
+ fontFamily: 'ui-sans-serif, system-ui, -apple-system',
1990
+ fontSize: '14px',
1013
1991
  fontWeight: 600,
1014
- color: "#ffffff",
1015
- boxShadow: "0 14px 24px rgba(15, 23, 42, 0.18)",
1016
- transition: "transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, color 0.2s ease",
1992
+ color: '#ffffff',
1993
+ boxShadow: '0 14px 24px rgba(15, 23, 42, 0.18)',
1994
+ transition: 'transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, color 0.2s ease',
1017
1995
  minWidth: 150,
1018
1996
  };
1019
1997
  const recordButtonStyle = {
1020
1998
  ...buttonBaseStyle,
1021
1999
  background: recording
1022
- ? "linear-gradient(120deg, #ef4444, #b91c1c)"
1023
- : "linear-gradient(120deg, #2563eb, #1d4ed8)",
2000
+ ? 'linear-gradient(120deg, #ef4444, #b91c1c)'
2001
+ : 'linear-gradient(120deg, #2563eb, #1d4ed8)',
1024
2002
  boxShadow: recording
1025
- ? "0 18px 32px rgba(239, 68, 68, 0.35)"
1026
- : "0 18px 32px rgba(37, 99, 235, 0.35)",
2003
+ ? '0 18px 32px rgba(239, 68, 68, 0.35)'
2004
+ : '0 18px 32px rgba(37, 99, 235, 0.35)',
1027
2005
  };
1028
2006
  const loginButtonStyle = {
1029
2007
  ...buttonBaseStyle,
1030
- background: "linear-gradient(120deg, #14b8a6, #0d9488)",
2008
+ background: 'linear-gradient(120deg, #14b8a6, #0d9488)',
1031
2009
  };
1032
2010
  const logoutButtonStyle = {
1033
2011
  ...buttonBaseStyle,
1034
- background: "#ffffff",
1035
- border: "1px solid #e5e7eb",
1036
- color: "#1f2933",
1037
- boxShadow: "0 10px 18px rgba(15, 23, 42, 0.12)",
2012
+ background: '#ffffff',
2013
+ border: '1px solid #e5e7eb',
2014
+ color: '#1f2933',
2015
+ boxShadow: '0 10px 18px rgba(15, 23, 42, 0.12)',
1038
2016
  };
1039
2017
  const shareCardStyle = {
1040
- width: "100%",
1041
- background: "#ffffff",
2018
+ width: '100%',
2019
+ background: '#ffffff',
1042
2020
  borderRadius: 16,
1043
- padding: "14px 16px",
1044
- border: "1px solid #e5e7eb",
1045
- boxShadow: "0 20px 36px rgba(15, 23, 42, 0.2)",
1046
- fontFamily: "ui-sans-serif, system-ui, -apple-system",
2021
+ padding: '14px 16px',
2022
+ border: '1px solid #e5e7eb',
2023
+ boxShadow: '0 20px 36px rgba(15, 23, 42, 0.2)',
2024
+ fontFamily: 'ui-sans-serif, system-ui, -apple-system',
1047
2025
  };
1048
2026
  const shareLinkStyle = {
1049
2027
  marginTop: 6,
1050
- padding: "8px 10px",
2028
+ padding: '8px 10px',
1051
2029
  borderRadius: 10,
1052
- background: "#f3f4f6",
2030
+ background: '#f3f4f6',
1053
2031
  fontSize: 12,
1054
- color: "#374151",
1055
- wordBreak: "break-all",
1056
- fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
2032
+ color: '#374151',
2033
+ wordBreak: 'break-all',
2034
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
1057
2035
  };
1058
2036
  const copyButtonStyle = {
1059
2037
  ...buttonBaseStyle,
1060
- padding: "8px 16px",
2038
+ padding: '8px 16px',
1061
2039
  fontSize: 13,
1062
- background: "#111827",
1063
- boxShadow: "none",
2040
+ background: '#111827',
2041
+ boxShadow: 'none',
1064
2042
  minWidth: 110,
1065
2043
  };
1066
2044
  const hideButtonStyle = {
1067
- border: "none",
1068
- background: "transparent",
1069
- color: "#6b7280",
1070
- cursor: "pointer",
2045
+ border: 'none',
2046
+ background: 'transparent',
2047
+ color: '#6b7280',
2048
+ cursor: 'pointer',
1071
2049
  fontWeight: 700,
1072
- padding: "6px 8px",
2050
+ padding: '6px 8px',
1073
2051
  borderRadius: 8,
1074
2052
  fontSize: 12,
1075
2053
  };
1076
2054
  const modalOverlayStyle = {
1077
- position: "fixed",
2055
+ position: 'fixed',
1078
2056
  inset: 0,
1079
- background: "rgba(15, 23, 42, 0.38)",
1080
- display: "flex",
1081
- alignItems: "center",
1082
- justifyContent: "center",
2057
+ background: 'rgba(15, 23, 42, 0.38)',
2058
+ display: 'flex',
2059
+ alignItems: 'center',
2060
+ justifyContent: 'center',
1083
2061
  zIndex: 2147483648,
1084
2062
  padding: 16,
1085
2063
  };
1086
2064
  const modalStyle = {
1087
- width: "100%",
2065
+ width: '100%',
1088
2066
  maxWidth: 360,
1089
- background: "#ffffff",
2067
+ background: '#ffffff',
1090
2068
  borderRadius: 12,
1091
- boxShadow: "0 20px 40px rgba(15, 23, 42, 0.25)",
1092
- padding: "24px 24px 20px",
1093
- fontFamily: "ui-sans-serif, system-ui, -apple-system",
2069
+ boxShadow: '0 20px 40px rgba(15, 23, 42, 0.25)',
2070
+ padding: '24px 24px 20px',
2071
+ fontFamily: 'ui-sans-serif, system-ui, -apple-system',
1094
2072
  };
1095
2073
  const hiddenNoticeStyle = {
1096
- position: "fixed",
2074
+ position: 'fixed',
1097
2075
  right: 16,
1098
2076
  bottom: 16,
1099
2077
  zIndex: 2147483647,
1100
2078
  maxWidth: 320,
1101
- background: "#111827",
1102
- color: "#e5e7eb",
2079
+ background: '#111827',
2080
+ color: '#e5e7eb',
1103
2081
  borderRadius: 12,
1104
- padding: "12px 14px",
1105
- boxShadow: "0 16px 30px rgba(0, 0, 0, 0.3)",
1106
- fontFamily: "ui-sans-serif, system-ui, -apple-system",
2082
+ padding: '12px 14px',
2083
+ boxShadow: '0 16px 30px rgba(0, 0, 0, 0.3)',
2084
+ fontFamily: 'ui-sans-serif, system-ui, -apple-system',
1107
2085
  fontSize: 13,
1108
2086
  lineHeight: 1.4,
1109
2087
  };
1110
2088
  const hiddenNoticeCloseStyle = {
1111
- border: "none",
1112
- background: "transparent",
1113
- color: "#e5e7eb",
1114
- cursor: "pointer",
2089
+ border: 'none',
2090
+ background: 'transparent',
2091
+ color: '#e5e7eb',
2092
+ cursor: 'pointer',
1115
2093
  fontWeight: 700,
1116
2094
  padding: 0,
1117
2095
  lineHeight: 1,
1118
2096
  fontSize: 14,
1119
2097
  };
1120
2098
  const labelStyle = {
1121
- display: "block",
2099
+ display: 'block',
1122
2100
  fontSize: 13,
1123
2101
  fontWeight: 600,
1124
- color: "#111827",
2102
+ color: '#111827',
1125
2103
  marginBottom: 6,
1126
2104
  };
1127
2105
  const inputStyle = {
1128
- width: "100%",
1129
- padding: "10px 12px",
2106
+ width: '100%',
2107
+ padding: '10px 12px',
1130
2108
  borderRadius: 6,
1131
- border: "1px solid #d1d5db",
2109
+ border: '1px solid #d1d5db',
1132
2110
  fontSize: 14,
1133
2111
  marginBottom: 14,
1134
- boxSizing: "border-box",
1135
- fontFamily: "inherit",
2112
+ boxSizing: 'border-box',
2113
+ fontFamily: 'inherit',
1136
2114
  };
1137
2115
  const hideControls = () => {
1138
2116
  setControlsHidden(true);
@@ -1143,119 +2121,149 @@ export function ReproProvider({ appId, children, button, masking, apiBase }) {
1143
2121
  return;
1144
2122
  const text = shareUrl;
1145
2123
  try {
1146
- if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
2124
+ if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
1147
2125
  await navigator.clipboard.writeText(text);
1148
2126
  }
1149
- else if (typeof document !== "undefined") {
1150
- const helper = document.createElement("textarea");
2127
+ else if (typeof document !== 'undefined') {
2128
+ const helper = document.createElement('textarea');
1151
2129
  helper.value = text;
1152
- helper.style.position = "fixed";
1153
- helper.style.opacity = "0";
1154
- helper.style.pointerEvents = "none";
2130
+ helper.style.position = 'fixed';
2131
+ helper.style.opacity = '0';
2132
+ helper.style.pointerEvents = 'none';
1155
2133
  document.body.appendChild(helper);
1156
2134
  helper.focus();
1157
2135
  helper.select();
1158
- document.execCommand("copy");
2136
+ document.execCommand('copy');
1159
2137
  document.body.removeChild(helper);
1160
2138
  }
1161
2139
  else {
1162
- throw new Error("clipboard unavailable");
2140
+ throw new Error('clipboard unavailable');
1163
2141
  }
1164
- setCopyStatusWithTimeout("copied");
2142
+ setCopyStatusWithTimeout('copied');
1165
2143
  }
1166
2144
  catch {
1167
- setCopyStatusWithTimeout("error");
2145
+ setCopyStatusWithTimeout('error');
1168
2146
  }
1169
2147
  }
1170
- return (_jsxs(_Fragment, { children: [children, !controlsHidden && (shareUrl || ready) && (_jsxs("div", { "data-repro-internal": "1", style: floatingContainerStyle, children: [_jsx("div", { "data-repro-internal": "1", style: { display: "flex", alignItems: "center", justifyContent: "flex-end", width: "100%", gap: 8 }, "aria-label": "Recording controls header", children: _jsx("button", { type: "button", "data-repro-internal": "1", onClick: hideControls, style: hideButtonStyle, "aria-label": "Hide recording controls", children: "Hide" }) }), shareUrl && (_jsxs("div", { "data-repro-internal": "1", style: shareCardStyle, children: [_jsxs("div", { style: {
1171
- display: "flex",
1172
- alignItems: "center",
1173
- justifyContent: "space-between",
2148
+ useEffect(() => {
2149
+ if (!ready || !isLiveMode) {
2150
+ return;
2151
+ }
2152
+ if (recordingRef.current) {
2153
+ return;
2154
+ }
2155
+ void start();
2156
+ }, [isLiveMode, ready]);
2157
+ useEffect(() => {
2158
+ if (!isLiveMode) {
2159
+ return;
2160
+ }
2161
+ return () => {
2162
+ if (recordingRef.current) {
2163
+ void stop();
2164
+ }
2165
+ };
2166
+ }, [isLiveMode]);
2167
+ return (_jsxs(_Fragment, { children: [children, !isLiveMode && !controlsHidden && (shareUrl || ready) && (_jsxs("div", { "data-repro-internal": "1", style: floatingContainerStyle, children: [_jsx("div", { "data-repro-internal": "1", style: {
2168
+ display: 'flex',
2169
+ alignItems: 'center',
2170
+ justifyContent: 'flex-end',
2171
+ width: '100%',
2172
+ gap: 8,
2173
+ }, "aria-label": "Recording controls header", children: _jsx("button", { type: "button", "data-repro-internal": "1", onClick: hideControls, style: hideButtonStyle, "aria-label": "Hide recording controls", children: "Hide" }) }), shareUrl && (_jsxs("div", { "data-repro-internal": "1", style: shareCardStyle, children: [_jsxs("div", { style: {
2174
+ display: 'flex',
2175
+ alignItems: 'center',
2176
+ justifyContent: 'space-between',
1174
2177
  gap: 12,
1175
- width: "100%",
1176
- }, children: [_jsx("div", { style: { fontSize: 12, fontWeight: 600, color: "#0f172a" }, children: "Latest capture ready to share" }), _jsx("button", { type: "button", "data-repro-internal": "1", onClick: () => clearShareInfo(), style: {
1177
- border: "none",
1178
- background: "transparent",
1179
- color: "#9ca3af",
1180
- cursor: "pointer",
2178
+ width: '100%',
2179
+ }, children: [_jsx("div", { style: { fontSize: 12, fontWeight: 600, color: '#0f172a' }, children: "Latest capture ready to share" }), _jsx("button", { type: "button", "data-repro-internal": "1", onClick: () => clearShareInfo(), style: {
2180
+ border: 'none',
2181
+ background: 'transparent',
2182
+ color: '#9ca3af',
2183
+ cursor: 'pointer',
1181
2184
  padding: 4,
1182
2185
  lineHeight: 1,
1183
2186
  fontWeight: 700,
1184
2187
  borderRadius: 999,
1185
2188
  width: 24,
1186
2189
  height: 24,
1187
- display: "flex",
1188
- alignItems: "center",
1189
- justifyContent: "center",
2190
+ display: 'flex',
2191
+ alignItems: 'center',
2192
+ justifyContent: 'center',
1190
2193
  }, "aria-label": "Close share link", children: "\u00D7" })] }), _jsx("div", { style: shareLinkStyle, title: shareUrl, children: shareUrl }), _jsxs("div", { style: {
1191
2194
  marginTop: 10,
1192
- display: "flex",
1193
- alignItems: "center",
1194
- justifyContent: "space-between",
2195
+ display: 'flex',
2196
+ alignItems: 'center',
2197
+ justifyContent: 'space-between',
1195
2198
  gap: 12,
1196
- width: "100%",
2199
+ width: '100%',
1197
2200
  }, children: [_jsx("span", { style: {
1198
2201
  fontSize: 12,
1199
2202
  minWidth: 100,
1200
- color: copyStatus === "copied"
1201
- ? "#059669"
1202
- : copyStatus === "error"
1203
- ? "#b91c1c"
1204
- : "#6b7280",
1205
- visibility: copyStatus === "idle" ? "hidden" : "visible",
1206
- transition: "color 0.2s ease",
1207
- }, children: copyStatus === "copied"
1208
- ? "Link copied!"
1209
- : copyStatus === "error"
1210
- ? "Unable to copy"
1211
- : "placeholder" }), _jsx("button", { type: "button", "data-repro-internal": "1", onClick: () => void copyShareLink(), style: copyButtonStyle, children: copyStatus === "copied" ? "Copied" : "Copy link" })] })] })), ready && auth && (_jsxs("div", { "data-repro-internal": "1", style: buttonRowStyle, children: [_jsxs("button", { "data-repro-internal": "1", onClick: () => (recording ? stop() : start()), style: recordButtonStyle, type: "button", children: [_jsx("span", { "aria-hidden": "true", style: {
2203
+ color: copyStatus === 'copied'
2204
+ ? '#059669'
2205
+ : copyStatus === 'error'
2206
+ ? '#b91c1c'
2207
+ : '#6b7280',
2208
+ visibility: copyStatus === 'idle' ? 'hidden' : 'visible',
2209
+ transition: 'color 0.2s ease',
2210
+ }, children: copyStatus === 'copied'
2211
+ ? 'Link copied!'
2212
+ : copyStatus === 'error'
2213
+ ? 'Unable to copy'
2214
+ : 'placeholder' }), _jsx("button", { type: "button", "data-repro-internal": "1", onClick: () => void copyShareLink(), style: copyButtonStyle, children: copyStatus === 'copied' ? 'Copied' : 'Copy link' })] })] })), ready && auth && (_jsxs("div", { "data-repro-internal": "1", style: buttonRowStyle, children: [_jsxs("button", { "data-repro-internal": "1", onClick: () => (recording ? stop() : start()), style: recordButtonStyle, type: "button", children: [_jsx("span", { "aria-hidden": "true", style: {
1212
2215
  width: 10,
1213
2216
  height: 10,
1214
- borderRadius: "999px",
1215
- background: recording ? "#fecaca" : "#bbf7d0",
2217
+ borderRadius: '999px',
2218
+ background: recording ? '#fecaca' : '#bbf7d0',
1216
2219
  boxShadow: recording
1217
- ? "0 0 12px rgba(239, 68, 68, 0.7)"
1218
- : "0 0 10px rgba(16, 185, 129, 0.6)",
2220
+ ? '0 0 12px rgba(239, 68, 68, 0.7)'
2221
+ : '0 0 10px rgba(16, 185, 129, 0.6)',
1219
2222
  } }), btnLabel] }), _jsx("button", { "data-repro-internal": "1", onClick: () => void logout(), style: logoutButtonStyle, type: "button", children: "Log out" })] })), ready && !auth && (_jsx("div", { "data-repro-internal": "1", style: buttonRowStyle, children: _jsx("button", { "data-repro-internal": "1", onClick: () => {
1220
2223
  setLoginError(null);
1221
2224
  setShowLogin(true);
1222
- }, style: loginButtonStyle, type: "button", children: loginLabel }) }))] })), controlsHidden && showHiddenNotice && (_jsxs("div", { "data-repro-internal": "1", style: hiddenNoticeStyle, children: [_jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }, children: [_jsx("div", { style: { fontWeight: 700 }, children: "Recording controls hidden" }), _jsx("button", { type: "button", "data-repro-internal": "1", style: hiddenNoticeCloseStyle, "aria-label": "Close hidden controls message", onClick: () => setShowHiddenNotice(false), children: "\u00D7" })] }), _jsx("div", { children: "Press Ctrl/Cmd + R + O to show them again." })] })), showLogin && (_jsx("div", { "data-repro-internal": "1", style: modalOverlayStyle, onClick: (evt) => {
2225
+ }, style: loginButtonStyle, type: "button", children: loginLabel }) }))] })), !isLiveMode && controlsHidden && showHiddenNotice && (_jsxs("div", { "data-repro-internal": "1", style: hiddenNoticeStyle, children: [_jsxs("div", { style: {
2226
+ display: 'flex',
2227
+ alignItems: 'center',
2228
+ justifyContent: 'space-between',
2229
+ gap: 12,
2230
+ }, children: [_jsx("div", { style: { fontWeight: 700 }, children: "Recording controls hidden" }), _jsx("button", { type: "button", "data-repro-internal": "1", style: hiddenNoticeCloseStyle, "aria-label": "Close hidden controls message", onClick: () => setShowHiddenNotice(false), children: "\u00D7" })] }), _jsx("div", { children: "Press Ctrl/Cmd + R + O to show them again." })] })), !isLiveMode && showLogin && (_jsx("div", { "data-repro-internal": "1", style: modalOverlayStyle, onClick: (evt) => {
1223
2231
  if (evt.target === evt.currentTarget && !isLoggingIn) {
1224
2232
  setShowLogin(false);
1225
2233
  }
1226
- }, children: _jsxs("div", { "data-repro-internal": "1", style: modalStyle, children: [_jsx("h3", { style: { margin: 0, marginBottom: 12, fontSize: 18, color: "#0f172a" }, children: "Authenticate to start recording" }), _jsxs("form", { onSubmit: handleLoginSubmit, children: [_jsx("label", { style: labelStyle, htmlFor: "repro-login-email", children: "Email" }), _jsx("input", { id: "repro-login-email", "data-repro-internal": "1", style: inputStyle, type: "email", value: loginEmail, placeholder: "user@example.com", onChange: (evt) => {
2234
+ }, children: _jsxs("div", { "data-repro-internal": "1", style: modalStyle, children: [_jsx("h3", { style: { margin: 0, marginBottom: 12, fontSize: 18, color: '#0f172a' }, children: "Authenticate to start recording" }), _jsxs("form", { onSubmit: handleLoginSubmit, children: [_jsx("label", { style: labelStyle, htmlFor: "repro-login-email", children: "Email" }), _jsx("input", { id: "repro-login-email", "data-repro-internal": "1", style: inputStyle, type: "email", value: loginEmail, placeholder: "user@example.com", onChange: (evt) => {
1227
2235
  setLoginEmail(evt.target.value);
1228
2236
  setLoginError(null);
1229
2237
  }, autoComplete: "email", required: true }), _jsx("label", { style: labelStyle, htmlFor: "repro-login-password", children: "Password" }), _jsx("input", { id: "repro-login-password", "data-repro-internal": "1", style: inputStyle, type: "password", value: loginPassword, placeholder: "Enter your workspace password", onChange: (evt) => {
1230
2238
  setLoginPassword(evt.target.value);
1231
2239
  setLoginError(null);
1232
2240
  }, autoComplete: "current-password", required: true }), loginError && (_jsx("div", { style: {
1233
- color: "#b91c1c",
1234
- background: "#fee2e2",
2241
+ color: '#b91c1c',
2242
+ background: '#fee2e2',
1235
2243
  borderRadius: 8,
1236
- padding: "8px 12px",
2244
+ padding: '8px 12px',
1237
2245
  fontSize: 13,
1238
2246
  marginBottom: 12,
1239
- }, children: loginError })), _jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 12 }, children: [_jsx("button", { type: "button", "data-repro-internal": "1", onClick: () => {
2247
+ }, children: loginError })), _jsxs("div", { style: { display: 'flex', justifyContent: 'flex-end', gap: 12 }, children: [_jsx("button", { type: "button", "data-repro-internal": "1", onClick: () => {
1240
2248
  if (!isLoggingIn)
1241
2249
  setShowLogin(false);
1242
2250
  }, style: {
1243
- padding: "10px 16px",
2251
+ padding: '10px 16px',
1244
2252
  borderRadius: 6,
1245
- border: "1px solid transparent",
1246
- background: "transparent",
1247
- color: "#4b5563",
2253
+ border: '1px solid transparent',
2254
+ background: 'transparent',
2255
+ color: '#4b5563',
1248
2256
  fontWeight: 600,
1249
- cursor: isLoggingIn ? "not-allowed" : "pointer",
2257
+ cursor: isLoggingIn ? 'not-allowed' : 'pointer',
1250
2258
  }, disabled: isLoggingIn, children: "Cancel" }), _jsx("button", { type: "submit", "data-repro-internal": "1", style: {
1251
- padding: "10px 16px",
2259
+ padding: '10px 16px',
1252
2260
  borderRadius: 6,
1253
- border: "none",
1254
- background: disableLogin ? "#d1d5db" : "#2563eb",
1255
- color: disableLogin ? "#6b7280" : "#ffffff",
2261
+ border: 'none',
2262
+ background: disableLogin ? '#d1d5db' : '#2563eb',
2263
+ color: disableLogin ? '#6b7280' : '#ffffff',
1256
2264
  fontWeight: 700,
1257
- cursor: disableLogin ? "not-allowed" : "pointer",
1258
- transition: "background 0.2s ease",
1259
- }, disabled: disableLogin, children: isLoggingIn ? "Signing in..." : "Sign in" })] })] })] }) }))] }));
2265
+ cursor: disableLogin ? 'not-allowed' : 'pointer',
2266
+ transition: 'background 0.2s ease',
2267
+ }, disabled: disableLogin, children: isLoggingIn ? 'Signing in...' : 'Sign in' })] })] })] }) }))] }));
1260
2268
  }
1261
2269
  export default ReproProvider;