@reproapp/react-sdk 0.0.1 → 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/README.md +30 -2
- package/dist/index.d.ts +28 -5
- package/dist/index.js +1459 -451
- package/dist/ingest-worker.d.ts +1 -0
- package/dist/ingest-worker.js +222 -0
- package/package.json +2 -1
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
|
|
3
|
-
import { record } from
|
|
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 =
|
|
47
|
-
const REQUEST_START_HEADER =
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
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
|
|
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 ===
|
|
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
|
-
|
|
79
|
-
|
|
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[
|
|
87
|
-
setHeader(
|
|
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(
|
|
168
|
+
setHeader('Authorization', `Bearer ${userToken}`);
|
|
93
169
|
}
|
|
94
170
|
}
|
|
95
|
-
if (sid && aid && !isInternal && !isSdkInternal) {
|
|
96
|
-
|
|
97
|
-
|
|
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(
|
|
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 ||
|
|
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
|
-
|
|
114
|
-
|
|
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 ||
|
|
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
|
-
|
|
130
|
-
|
|
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, }) {
|
|
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 ===
|
|
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 !==
|
|
312
|
+
if (!parsed || typeof parsed !== 'object')
|
|
141
313
|
return null;
|
|
142
|
-
const email = typeof parsed.email ===
|
|
143
|
-
const password = typeof parsed.password ===
|
|
144
|
-
const token = typeof parsed.token ===
|
|
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 }) {
|
|
|
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 }) {
|
|
|
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 }) {
|
|
|
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(
|
|
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 }) {
|
|
|
201
386
|
const clearCopyStatusTimer = () => {
|
|
202
387
|
if (copyStatusTimerRef.current == null)
|
|
203
388
|
return;
|
|
204
|
-
if (typeof window !==
|
|
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 }) {
|
|
|
211
396
|
};
|
|
212
397
|
const resetCopyFeedback = () => {
|
|
213
398
|
clearCopyStatusTimer();
|
|
214
|
-
setCopyStatus(
|
|
399
|
+
setCopyStatus('idle');
|
|
215
400
|
};
|
|
216
401
|
const clearShareInfo = () => {
|
|
217
402
|
setShareUrl(null);
|
|
@@ -220,11 +405,11 @@ export function ReproProvider({ appId, children, button, masking }) {
|
|
|
220
405
|
const setCopyStatusWithTimeout = (status) => {
|
|
221
406
|
clearCopyStatusTimer();
|
|
222
407
|
setCopyStatus(status);
|
|
223
|
-
if (status ===
|
|
408
|
+
if (status === 'idle')
|
|
224
409
|
return;
|
|
225
|
-
if (typeof window !==
|
|
410
|
+
if (typeof window !== 'undefined') {
|
|
226
411
|
copyStatusTimerRef.current = window.setTimeout(() => {
|
|
227
|
-
setCopyStatus(
|
|
412
|
+
setCopyStatus('idle');
|
|
228
413
|
copyStatusTimerRef.current = null;
|
|
229
414
|
}, 2200);
|
|
230
415
|
}
|
|
@@ -232,7 +417,7 @@ export function ReproProvider({ appId, children, button, masking }) {
|
|
|
232
417
|
const clearSessionExpiryTimer = () => {
|
|
233
418
|
if (sessionExpiryTimerRef.current == null)
|
|
234
419
|
return;
|
|
235
|
-
if (typeof window !==
|
|
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 }) {
|
|
|
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 }) {
|
|
|
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 }) {
|
|
|
257
959
|
getToken: () => sdkTokenRef.current,
|
|
258
960
|
getUserToken: () => userTokenRef.current,
|
|
259
961
|
getUserPassword: () => userPasswordRef.current,
|
|
260
|
-
getFetch: () =>
|
|
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 }) {
|
|
|
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 }) {
|
|
|
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:
|
|
998
|
+
method: 'GET',
|
|
294
999
|
headers: addTenantHeader({
|
|
295
|
-
Accept:
|
|
1000
|
+
Accept: 'application/json',
|
|
296
1001
|
Authorization: `Bearer ${storedToken}`,
|
|
297
|
-
|
|
298
|
-
[INTERNAL_HEADER]:
|
|
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 }) {
|
|
|
320
1025
|
useEffect(() => {
|
|
321
1026
|
userPasswordRef.current = auth?.password ?? null;
|
|
322
1027
|
userTokenRef.current = auth?.token ?? null;
|
|
323
|
-
if (typeof window !==
|
|
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 }) {
|
|
|
346
1051
|
setLoginError(null);
|
|
347
1052
|
try {
|
|
348
1053
|
const resp = await fetch(`${base}/v1/apps/${appId}/users/login`, {
|
|
349
|
-
method:
|
|
1054
|
+
method: 'POST',
|
|
350
1055
|
headers: addTenantHeader({
|
|
351
|
-
Accept:
|
|
352
|
-
|
|
353
|
-
[INTERNAL_HEADER]:
|
|
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 }) {
|
|
|
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 ===
|
|
1066
|
+
const accessTokenFromUser = typeof data?.user?.accessToken === 'string'
|
|
362
1067
|
? data.user.accessToken.trim()
|
|
363
1068
|
: null;
|
|
364
|
-
const accessTokenFromData = typeof data?.accessToken ===
|
|
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(
|
|
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 ||
|
|
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 }) {
|
|
|
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 }) {
|
|
|
408
1111
|
}
|
|
409
1112
|
}
|
|
410
1113
|
function handleUnauthorized() {
|
|
411
|
-
void logout({ showLogin:
|
|
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 }) {
|
|
|
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 ===
|
|
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 }) {
|
|
|
438
1142
|
pressed.clear();
|
|
439
1143
|
return;
|
|
440
1144
|
}
|
|
441
|
-
const key = (evt.key ||
|
|
442
|
-
if (key ===
|
|
1145
|
+
const key = (evt.key || '').toLowerCase();
|
|
1146
|
+
if (key === 'r' || key === 'o') {
|
|
443
1147
|
pressed.add(key);
|
|
444
|
-
const combo = pressed.has(
|
|
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 }) {
|
|
|
456
1160
|
}
|
|
457
1161
|
return;
|
|
458
1162
|
}
|
|
459
|
-
if (key ===
|
|
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 ||
|
|
467
|
-
if (key ===
|
|
1170
|
+
const key = (evt.key || '').toLowerCase();
|
|
1171
|
+
if (key === 'r' || key === 'o') {
|
|
468
1172
|
pressed.delete(key);
|
|
469
1173
|
}
|
|
470
|
-
else if (key ===
|
|
1174
|
+
else if (key === 'control' || key === 'meta') {
|
|
471
1175
|
pressed.clear();
|
|
472
1176
|
}
|
|
473
1177
|
};
|
|
474
|
-
window.addEventListener(
|
|
475
|
-
window.addEventListener(
|
|
1178
|
+
window.addEventListener('keydown', handleKeyDown, true);
|
|
1179
|
+
window.addEventListener('keyup', handleKeyUp, true);
|
|
476
1180
|
return () => {
|
|
477
|
-
window.removeEventListener(
|
|
478
|
-
window.removeEventListener(
|
|
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 }) {
|
|
|
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]:
|
|
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 }) {
|
|
|
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
|
-
|
|
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:
|
|
1370
|
+
method: 'POST',
|
|
513
1371
|
headers: addTenantHeader({
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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]:
|
|
1376
|
+
[INTERNAL_HEADER]: '1',
|
|
519
1377
|
}),
|
|
520
1378
|
body: gz,
|
|
521
1379
|
});
|
|
@@ -531,20 +1389,40 @@ export function ReproProvider({ appId, children, button, masking }) {
|
|
|
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
|
-
const baseUrl =
|
|
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 }) {
|
|
|
564
1442
|
const env = mkEnvelope(piece);
|
|
565
1443
|
const jsonSize = jsonBytes(env);
|
|
566
1444
|
let res;
|
|
567
|
-
if (
|
|
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 }) {
|
|
|
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:
|
|
1486
|
+
method: 'POST',
|
|
604
1487
|
headers: addTenantHeader({
|
|
605
|
-
|
|
606
|
-
|
|
1488
|
+
'Content-Type': 'application/json',
|
|
1489
|
+
'x-sdk-token': token,
|
|
607
1490
|
...(userTokenRef.current ? { Authorization: `Bearer ${userTokenRef.current}` } : {}),
|
|
608
|
-
[INTERNAL_HEADER]:
|
|
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 }) {
|
|
|
631
1514
|
return;
|
|
632
1515
|
ax.__reproAttached = true;
|
|
633
1516
|
ax.interceptors.request.use((config) => {
|
|
634
|
-
const
|
|
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
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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(
|
|
651
|
-
const url = `${resp.config.baseURL ||
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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) =>
|
|
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
|
|
698
|
-
const txt = (el.innerText || el.getAttribute?.(
|
|
699
|
-
el.getAttribute?.(
|
|
700
|
-
|
|
701
|
-
const id = el.id ? `#${el.id}` :
|
|
702
|
-
const cls = el.className && typeof el.className ===
|
|
703
|
-
? `.${el.className.split(
|
|
704
|
-
:
|
|
705
|
-
return txt ? `Click • ${txt}` : `Click • ${(el.tagName ||
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1658
|
+
if (!isLiveMode) {
|
|
1659
|
+
rrFlushTimerRef.current = window.setInterval(() => {
|
|
1660
|
+
void flushRrwebBuffer('timer');
|
|
1661
|
+
}, FLUSH_MS);
|
|
1662
|
+
}
|
|
740
1663
|
window.fetch = async (input, init = {}) => {
|
|
741
|
-
|
|
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
|
-
|
|
750
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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(
|
|
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 &&
|
|
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
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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(
|
|
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(
|
|
1824
|
+
document.removeEventListener('click', clickHandlerRef.current, { capture: true });
|
|
853
1825
|
clickHandlerRef.current = null;
|
|
854
1826
|
}
|
|
855
1827
|
};
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
-
|
|
919
|
-
|
|
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
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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 (
|
|
945
|
-
|
|
946
|
-
|
|
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
|
-
|
|
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 }) {
|
|
|
975
1953
|
}
|
|
976
1954
|
}
|
|
977
1955
|
// ---- UI (floating button) ----
|
|
978
|
-
const btnLabel = recording ?
|
|
979
|
-
const loginLabel =
|
|
1956
|
+
const btnLabel = recording ? button?.text ?? 'Stop & Report' : button?.text ?? 'Record';
|
|
1957
|
+
const loginLabel = 'Authenticate to Record';
|
|
980
1958
|
const floatingContainerBaseStyle = {
|
|
981
|
-
position:
|
|
1959
|
+
position: 'fixed',
|
|
982
1960
|
zIndex: 2147483647,
|
|
983
|
-
display:
|
|
984
|
-
flexDirection:
|
|
985
|
-
alignItems:
|
|
1961
|
+
display: 'flex',
|
|
1962
|
+
flexDirection: 'column',
|
|
1963
|
+
alignItems: 'flex-end',
|
|
986
1964
|
gap: 12,
|
|
987
|
-
width:
|
|
1965
|
+
width: '100%',
|
|
988
1966
|
maxWidth: 360,
|
|
989
1967
|
};
|
|
990
1968
|
const floatingContainerStyle = {
|
|
@@ -993,146 +1971,146 @@ export function ReproProvider({ appId, children, button, masking }) {
|
|
|
993
1971
|
bottom: 16,
|
|
994
1972
|
};
|
|
995
1973
|
const buttonRowStyle = {
|
|
996
|
-
display:
|
|
1974
|
+
display: 'flex',
|
|
997
1975
|
gap: 10,
|
|
998
|
-
justifyContent:
|
|
999
|
-
width:
|
|
1976
|
+
justifyContent: 'flex-end',
|
|
1977
|
+
width: '100%',
|
|
1000
1978
|
};
|
|
1001
1979
|
const buttonBaseStyle = {
|
|
1002
|
-
display:
|
|
1003
|
-
alignItems:
|
|
1004
|
-
justifyContent:
|
|
1980
|
+
display: 'inline-flex',
|
|
1981
|
+
alignItems: 'center',
|
|
1982
|
+
justifyContent: 'center',
|
|
1005
1983
|
gap: 8,
|
|
1006
|
-
padding:
|
|
1984
|
+
padding: '12px 24px',
|
|
1007
1985
|
borderRadius: 9999,
|
|
1008
|
-
border:
|
|
1009
|
-
background:
|
|
1010
|
-
cursor:
|
|
1011
|
-
fontFamily:
|
|
1012
|
-
fontSize:
|
|
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:
|
|
1015
|
-
boxShadow:
|
|
1016
|
-
transition:
|
|
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
|
-
?
|
|
1023
|
-
:
|
|
2000
|
+
? 'linear-gradient(120deg, #ef4444, #b91c1c)'
|
|
2001
|
+
: 'linear-gradient(120deg, #2563eb, #1d4ed8)',
|
|
1024
2002
|
boxShadow: recording
|
|
1025
|
-
?
|
|
1026
|
-
:
|
|
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:
|
|
2008
|
+
background: 'linear-gradient(120deg, #14b8a6, #0d9488)',
|
|
1031
2009
|
};
|
|
1032
2010
|
const logoutButtonStyle = {
|
|
1033
2011
|
...buttonBaseStyle,
|
|
1034
|
-
background:
|
|
1035
|
-
border:
|
|
1036
|
-
color:
|
|
1037
|
-
boxShadow:
|
|
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:
|
|
1041
|
-
background:
|
|
2018
|
+
width: '100%',
|
|
2019
|
+
background: '#ffffff',
|
|
1042
2020
|
borderRadius: 16,
|
|
1043
|
-
padding:
|
|
1044
|
-
border:
|
|
1045
|
-
boxShadow:
|
|
1046
|
-
fontFamily:
|
|
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:
|
|
2028
|
+
padding: '8px 10px',
|
|
1051
2029
|
borderRadius: 10,
|
|
1052
|
-
background:
|
|
2030
|
+
background: '#f3f4f6',
|
|
1053
2031
|
fontSize: 12,
|
|
1054
|
-
color:
|
|
1055
|
-
wordBreak:
|
|
1056
|
-
fontFamily:
|
|
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:
|
|
2038
|
+
padding: '8px 16px',
|
|
1061
2039
|
fontSize: 13,
|
|
1062
|
-
background:
|
|
1063
|
-
boxShadow:
|
|
2040
|
+
background: '#111827',
|
|
2041
|
+
boxShadow: 'none',
|
|
1064
2042
|
minWidth: 110,
|
|
1065
2043
|
};
|
|
1066
2044
|
const hideButtonStyle = {
|
|
1067
|
-
border:
|
|
1068
|
-
background:
|
|
1069
|
-
color:
|
|
1070
|
-
cursor:
|
|
2045
|
+
border: 'none',
|
|
2046
|
+
background: 'transparent',
|
|
2047
|
+
color: '#6b7280',
|
|
2048
|
+
cursor: 'pointer',
|
|
1071
2049
|
fontWeight: 700,
|
|
1072
|
-
padding:
|
|
2050
|
+
padding: '6px 8px',
|
|
1073
2051
|
borderRadius: 8,
|
|
1074
2052
|
fontSize: 12,
|
|
1075
2053
|
};
|
|
1076
2054
|
const modalOverlayStyle = {
|
|
1077
|
-
position:
|
|
2055
|
+
position: 'fixed',
|
|
1078
2056
|
inset: 0,
|
|
1079
|
-
background:
|
|
1080
|
-
display:
|
|
1081
|
-
alignItems:
|
|
1082
|
-
justifyContent:
|
|
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:
|
|
2065
|
+
width: '100%',
|
|
1088
2066
|
maxWidth: 360,
|
|
1089
|
-
background:
|
|
2067
|
+
background: '#ffffff',
|
|
1090
2068
|
borderRadius: 12,
|
|
1091
|
-
boxShadow:
|
|
1092
|
-
padding:
|
|
1093
|
-
fontFamily:
|
|
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:
|
|
2074
|
+
position: 'fixed',
|
|
1097
2075
|
right: 16,
|
|
1098
2076
|
bottom: 16,
|
|
1099
2077
|
zIndex: 2147483647,
|
|
1100
2078
|
maxWidth: 320,
|
|
1101
|
-
background:
|
|
1102
|
-
color:
|
|
2079
|
+
background: '#111827',
|
|
2080
|
+
color: '#e5e7eb',
|
|
1103
2081
|
borderRadius: 12,
|
|
1104
|
-
padding:
|
|
1105
|
-
boxShadow:
|
|
1106
|
-
fontFamily:
|
|
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:
|
|
1112
|
-
background:
|
|
1113
|
-
color:
|
|
1114
|
-
cursor:
|
|
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:
|
|
2099
|
+
display: 'block',
|
|
1122
2100
|
fontSize: 13,
|
|
1123
2101
|
fontWeight: 600,
|
|
1124
|
-
color:
|
|
2102
|
+
color: '#111827',
|
|
1125
2103
|
marginBottom: 6,
|
|
1126
2104
|
};
|
|
1127
2105
|
const inputStyle = {
|
|
1128
|
-
width:
|
|
1129
|
-
padding:
|
|
2106
|
+
width: '100%',
|
|
2107
|
+
padding: '10px 12px',
|
|
1130
2108
|
borderRadius: 6,
|
|
1131
|
-
border:
|
|
2109
|
+
border: '1px solid #d1d5db',
|
|
1132
2110
|
fontSize: 14,
|
|
1133
2111
|
marginBottom: 14,
|
|
1134
|
-
boxSizing:
|
|
1135
|
-
fontFamily:
|
|
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 }) {
|
|
|
1143
2121
|
return;
|
|
1144
2122
|
const text = shareUrl;
|
|
1145
2123
|
try {
|
|
1146
|
-
if (typeof navigator !==
|
|
2124
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
|
1147
2125
|
await navigator.clipboard.writeText(text);
|
|
1148
2126
|
}
|
|
1149
|
-
else if (typeof document !==
|
|
1150
|
-
const helper = document.createElement(
|
|
2127
|
+
else if (typeof document !== 'undefined') {
|
|
2128
|
+
const helper = document.createElement('textarea');
|
|
1151
2129
|
helper.value = text;
|
|
1152
|
-
helper.style.position =
|
|
1153
|
-
helper.style.opacity =
|
|
1154
|
-
helper.style.pointerEvents =
|
|
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(
|
|
2136
|
+
document.execCommand('copy');
|
|
1159
2137
|
document.body.removeChild(helper);
|
|
1160
2138
|
}
|
|
1161
2139
|
else {
|
|
1162
|
-
throw new Error(
|
|
2140
|
+
throw new Error('clipboard unavailable');
|
|
1163
2141
|
}
|
|
1164
|
-
setCopyStatusWithTimeout(
|
|
2142
|
+
setCopyStatusWithTimeout('copied');
|
|
1165
2143
|
}
|
|
1166
2144
|
catch {
|
|
1167
|
-
setCopyStatusWithTimeout(
|
|
2145
|
+
setCopyStatusWithTimeout('error');
|
|
1168
2146
|
}
|
|
1169
2147
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
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:
|
|
1176
|
-
}, children: [_jsx("div", { style: { fontSize: 12, fontWeight: 600, color:
|
|
1177
|
-
border:
|
|
1178
|
-
background:
|
|
1179
|
-
color:
|
|
1180
|
-
cursor:
|
|
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:
|
|
1188
|
-
alignItems:
|
|
1189
|
-
justifyContent:
|
|
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:
|
|
1193
|
-
alignItems:
|
|
1194
|
-
justifyContent:
|
|
2195
|
+
display: 'flex',
|
|
2196
|
+
alignItems: 'center',
|
|
2197
|
+
justifyContent: 'space-between',
|
|
1195
2198
|
gap: 12,
|
|
1196
|
-
width:
|
|
2199
|
+
width: '100%',
|
|
1197
2200
|
}, children: [_jsx("span", { style: {
|
|
1198
2201
|
fontSize: 12,
|
|
1199
2202
|
minWidth: 100,
|
|
1200
|
-
color: copyStatus ===
|
|
1201
|
-
?
|
|
1202
|
-
: copyStatus ===
|
|
1203
|
-
?
|
|
1204
|
-
:
|
|
1205
|
-
visibility: copyStatus ===
|
|
1206
|
-
transition:
|
|
1207
|
-
}, children: copyStatus ===
|
|
1208
|
-
?
|
|
1209
|
-
: copyStatus ===
|
|
1210
|
-
?
|
|
1211
|
-
:
|
|
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:
|
|
1215
|
-
background: recording ?
|
|
2217
|
+
borderRadius: '999px',
|
|
2218
|
+
background: recording ? '#fecaca' : '#bbf7d0',
|
|
1216
2219
|
boxShadow: recording
|
|
1217
|
-
?
|
|
1218
|
-
:
|
|
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: {
|
|
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:
|
|
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:
|
|
1234
|
-
background:
|
|
2241
|
+
color: '#b91c1c',
|
|
2242
|
+
background: '#fee2e2',
|
|
1235
2243
|
borderRadius: 8,
|
|
1236
|
-
padding:
|
|
2244
|
+
padding: '8px 12px',
|
|
1237
2245
|
fontSize: 13,
|
|
1238
2246
|
marginBottom: 12,
|
|
1239
|
-
}, children: loginError })), _jsxs("div", { style: { display:
|
|
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:
|
|
2251
|
+
padding: '10px 16px',
|
|
1244
2252
|
borderRadius: 6,
|
|
1245
|
-
border:
|
|
1246
|
-
background:
|
|
1247
|
-
color:
|
|
2253
|
+
border: '1px solid transparent',
|
|
2254
|
+
background: 'transparent',
|
|
2255
|
+
color: '#4b5563',
|
|
1248
2256
|
fontWeight: 600,
|
|
1249
|
-
cursor: isLoggingIn ?
|
|
2257
|
+
cursor: isLoggingIn ? 'not-allowed' : 'pointer',
|
|
1250
2258
|
}, disabled: isLoggingIn, children: "Cancel" }), _jsx("button", { type: "submit", "data-repro-internal": "1", style: {
|
|
1251
|
-
padding:
|
|
2259
|
+
padding: '10px 16px',
|
|
1252
2260
|
borderRadius: 6,
|
|
1253
|
-
border:
|
|
1254
|
-
background: disableLogin ?
|
|
1255
|
-
color: disableLogin ?
|
|
2261
|
+
border: 'none',
|
|
2262
|
+
background: disableLogin ? '#d1d5db' : '#2563eb',
|
|
2263
|
+
color: disableLogin ? '#6b7280' : '#ffffff',
|
|
1256
2264
|
fontWeight: 700,
|
|
1257
|
-
cursor: disableLogin ?
|
|
1258
|
-
transition:
|
|
1259
|
-
}, disabled: disableLogin, children: isLoggingIn ?
|
|
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;
|