@sailfish-ai/recorder 1.7.11 → 1.7.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +346 -50
- package/dist/sailfish-recorder.cjs.js +1 -1
- package/dist/sailfish-recorder.cjs.js.br +0 -0
- package/dist/sailfish-recorder.cjs.js.gz +0 -0
- package/dist/sailfish-recorder.es.js +1 -1
- package/dist/sailfish-recorder.es.js.br +0 -0
- package/dist/sailfish-recorder.es.js.gz +0 -0
- package/dist/sailfish-recorder.umd.js +1 -1
- package/dist/sailfish-recorder.umd.js.br +0 -0
- package/dist/sailfish-recorder.umd.js.gz +0 -0
- package/dist/types/index.d.ts +3 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
const DEBUG = import.meta.env.VITE_DEBUG ? import.meta.env.VITE_DEBUG : false;
|
|
1
2
|
// import { NetworkRecordOptions } from "@sailfish-rrweb/rrweb-plugin-network-record";
|
|
2
3
|
import { v4 as uuidv4 } from "uuid";
|
|
4
|
+
import { NetworkRequestEventId, xSf3RidHeader } from "./constants";
|
|
3
5
|
import { gatherAndCacheDeviceInfo } from "./deviceInfo";
|
|
4
6
|
import { fetchCaptureSettings, sendDomainsToNotPropagateHeaderTo, startRecordingSession, } from "./graphql";
|
|
5
7
|
import { sendMapUuidIfAvailable } from "./mapUuid";
|
|
6
|
-
import {
|
|
8
|
+
import { getUrlAndStoredUuids, initializeConsolePlugin, initializeDomContentEvents, initializeRecording, } from "./recording";
|
|
7
9
|
import { sendEvent, sendMessage } from "./websocket";
|
|
8
|
-
import { NetworkRequestEventId, xSf3RidHeader } from "./constants";
|
|
9
10
|
// Default list of domains to ignore
|
|
10
11
|
const DOMAINS_TO_NOT_PROPAGATE_HEADER_TO_DEFAULT = [
|
|
11
12
|
"t.co",
|
|
@@ -16,6 +17,84 @@ const DOMAINS_TO_NOT_PROPAGATE_HEADER_TO_DEFAULT = [
|
|
|
16
17
|
"*.smooch.io", // Exclude smooch-related requests
|
|
17
18
|
"*.zendesk.com", // Exclude zendesk-related requests
|
|
18
19
|
];
|
|
20
|
+
const BAD_HTTP_STATUS = [
|
|
21
|
+
400, // BAD REQUEST
|
|
22
|
+
403, // FORBIDDEN
|
|
23
|
+
];
|
|
24
|
+
const CORS_KEYWORD = "CORS";
|
|
25
|
+
const DYNAMIC_PASSED_HOSTS_KEY = "dynamicPassedHosts";
|
|
26
|
+
const DYNAMIC_EXCLUDED_HOSTS_KEY = "dynamicExcludedHosts";
|
|
27
|
+
const SF_API_KEY_FOR_UPDATE = "sailfishApiKey";
|
|
28
|
+
const SF_BACKEND_API = "sailfishBackendApi";
|
|
29
|
+
const INCLUDE = "include";
|
|
30
|
+
const SAME_ORIGIN = "same-origin";
|
|
31
|
+
/**
|
|
32
|
+
* Notify the backend of the updated dynamicExcludedHosts
|
|
33
|
+
*/
|
|
34
|
+
function updateExcludedHostsStorageAndBackend(dynamicExcludedHosts) {
|
|
35
|
+
const apiKeyForUpdate = sessionStorage.getItem(SF_API_KEY_FOR_UPDATE) || "";
|
|
36
|
+
const apiForUpdate = sessionStorage.getItem(SF_BACKEND_API) || "";
|
|
37
|
+
if (!apiForUpdate) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
sendDomainsToNotPropagateHeaderTo(apiKeyForUpdate, [...dynamicExcludedHosts, ...DOMAINS_TO_NOT_PROPAGATE_HEADER_TO_DEFAULT], apiForUpdate).catch((error) => console.error("Failed to send domains to not propagate header to:", error));
|
|
41
|
+
}
|
|
42
|
+
const dynamicExcludedHosts = new Set();
|
|
43
|
+
const dynamicPassedHosts = new Set();
|
|
44
|
+
// Load initial dynamicExcludedHosts from localStorage
|
|
45
|
+
(() => {
|
|
46
|
+
const stored = localStorage.getItem(DYNAMIC_EXCLUDED_HOSTS_KEY);
|
|
47
|
+
if (stored) {
|
|
48
|
+
try {
|
|
49
|
+
JSON.parse(stored).forEach((host) => dynamicExcludedHosts.add(host));
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
if (DEBUG)
|
|
53
|
+
console.log("Failed to parse dynamicExcludedHosts from storage", e);
|
|
54
|
+
localStorage.removeItem(DYNAMIC_EXCLUDED_HOSTS_KEY);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
})();
|
|
58
|
+
// Load initial dynamicPassedHosts from localStorage
|
|
59
|
+
(() => {
|
|
60
|
+
const stored = localStorage.getItem(DYNAMIC_PASSED_HOSTS_KEY);
|
|
61
|
+
if (stored) {
|
|
62
|
+
try {
|
|
63
|
+
JSON.parse(stored).forEach((host) => dynamicPassedHosts.add(host));
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
if (DEBUG)
|
|
67
|
+
console.log("Failed to parse dynamicPassedHosts from storage", e);
|
|
68
|
+
localStorage.removeItem(DYNAMIC_PASSED_HOSTS_KEY);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
})();
|
|
72
|
+
// Override add() to persist updates to localStorage
|
|
73
|
+
const originalExcludedAdd = dynamicExcludedHosts.add;
|
|
74
|
+
dynamicExcludedHosts.add = (host) => {
|
|
75
|
+
const cleanedHost = host?.trim();
|
|
76
|
+
if (!cleanedHost) {
|
|
77
|
+
return dynamicExcludedHosts;
|
|
78
|
+
}
|
|
79
|
+
originalExcludedAdd.call(dynamicExcludedHosts, cleanedHost);
|
|
80
|
+
localStorage.setItem(DYNAMIC_EXCLUDED_HOSTS_KEY, JSON.stringify(Array.from(dynamicExcludedHosts)));
|
|
81
|
+
updateExcludedHostsStorageAndBackend(dynamicExcludedHosts);
|
|
82
|
+
return dynamicExcludedHosts;
|
|
83
|
+
};
|
|
84
|
+
const originalPassedAdd = dynamicPassedHosts.add;
|
|
85
|
+
dynamicPassedHosts.add = (host) => {
|
|
86
|
+
const cleanedHost = host?.trim();
|
|
87
|
+
if (!cleanedHost) {
|
|
88
|
+
return dynamicPassedHosts;
|
|
89
|
+
}
|
|
90
|
+
originalPassedAdd.call(dynamicPassedHosts, cleanedHost);
|
|
91
|
+
localStorage.setItem(DYNAMIC_PASSED_HOSTS_KEY, JSON.stringify(Array.from(dynamicPassedHosts)));
|
|
92
|
+
return dynamicPassedHosts;
|
|
93
|
+
};
|
|
94
|
+
const ActionType = {
|
|
95
|
+
PROPAGATE: "propagate",
|
|
96
|
+
IGNORE: "ignore",
|
|
97
|
+
};
|
|
19
98
|
export const DEFAULT_CAPTURE_SETTINGS = {
|
|
20
99
|
recordCanvas: false,
|
|
21
100
|
recordCrossOriginIframes: false,
|
|
@@ -215,19 +294,88 @@ export function matchUrlWithWildcard(url, patterns) {
|
|
|
215
294
|
return true;
|
|
216
295
|
});
|
|
217
296
|
}
|
|
297
|
+
function shouldSkipHeadersPropagation(url, domainsToNotPropagateHeadersTo = []) {
|
|
298
|
+
const combinedIgnoreDomains = [
|
|
299
|
+
...DOMAINS_TO_NOT_PROPAGATE_HEADER_TO_DEFAULT,
|
|
300
|
+
...domainsToNotPropagateHeadersTo,
|
|
301
|
+
];
|
|
302
|
+
const defaultExcluded = matchUrlWithWildcard(url, combinedIgnoreDomains);
|
|
303
|
+
if (defaultExcluded) {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
const domain = new URL(url).hostname;
|
|
307
|
+
// Check dynamically excluded hosts (those that reject the tracing header runtime)
|
|
308
|
+
return dynamicExcludedHosts.has(domain);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Performs an OPTIONS preflight check using XHR.
|
|
312
|
+
* Returns ActionType.PROPAGATE if server responds 2xx, ActionType.IGNORE on error or non-2xx.
|
|
313
|
+
*/
|
|
314
|
+
function performOptionsPreflightForXHR(url, init, xSf3RidHeaderValue, domain) {
|
|
315
|
+
return new Promise((resolve) => {
|
|
316
|
+
const xhr = new XMLHttpRequest();
|
|
317
|
+
xhr.open("OPTIONS", url, true);
|
|
318
|
+
// Mirror credentials
|
|
319
|
+
xhr.withCredentials = init.credentials === INCLUDE;
|
|
320
|
+
// CORS preflight headers
|
|
321
|
+
const method = (init.method || "GET").toUpperCase();
|
|
322
|
+
xhr.setRequestHeader("Access-Control-Request-Method", method);
|
|
323
|
+
const rawHeaders = init.headers instanceof Headers
|
|
324
|
+
? Object.entries(init.headers)
|
|
325
|
+
: init.headers || {};
|
|
326
|
+
const customHeaders = Object.keys(rawHeaders)
|
|
327
|
+
.map((h) => h.toLowerCase())
|
|
328
|
+
.filter((h) => ![
|
|
329
|
+
"accept",
|
|
330
|
+
"content-type",
|
|
331
|
+
"accept-language",
|
|
332
|
+
"content-language",
|
|
333
|
+
].includes(h));
|
|
334
|
+
if (customHeaders.length) {
|
|
335
|
+
xhr.setRequestHeader("Access-Control-Request-Headers", customHeaders.join(","));
|
|
336
|
+
}
|
|
337
|
+
// Add tracing header
|
|
338
|
+
this.setRequestHeader(xSf3RidHeader, xSf3RidHeaderValue);
|
|
339
|
+
xhr.onload = () => {
|
|
340
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
341
|
+
resolve(ActionType.PROPAGATE);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
DEBUG &&
|
|
345
|
+
console.log(`[XHR Interceptor] OPTIONS returned status ${xhr.status} for ${domain}`);
|
|
346
|
+
resolve(null);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
xhr.onerror = () => {
|
|
350
|
+
DEBUG &&
|
|
351
|
+
console.log(`[XHR Interceptor] Preflight OPTIONS CORS or network error for ${domain}`);
|
|
352
|
+
resolve(ActionType.IGNORE);
|
|
353
|
+
};
|
|
354
|
+
xhr.send();
|
|
355
|
+
});
|
|
356
|
+
}
|
|
218
357
|
// Updated XMLHttpRequest interceptor with domain exclusion
|
|
219
|
-
function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo
|
|
358
|
+
function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo = []) {
|
|
220
359
|
const originalOpen = XMLHttpRequest.prototype.open;
|
|
221
360
|
const originalSend = XMLHttpRequest.prototype.send;
|
|
361
|
+
const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
|
|
222
362
|
const sessionId = getOrSetSessionId();
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
363
|
+
// Intercept setRequestHeader()
|
|
364
|
+
XMLHttpRequest.prototype.setRequestHeader = function (name, value) {
|
|
365
|
+
// initialize the buffer on first use
|
|
366
|
+
if (!this._capturedRequestHeaders) {
|
|
367
|
+
this._capturedRequestHeaders = {};
|
|
368
|
+
}
|
|
369
|
+
// store header name + value
|
|
370
|
+
this._capturedRequestHeaders[name] = value;
|
|
371
|
+
// still call the native method so the header actually goes on the wire
|
|
372
|
+
return originalSetRequestHeader.call(this, name, value);
|
|
373
|
+
};
|
|
228
374
|
// Intercept open()
|
|
229
375
|
XMLHttpRequest.prototype.open = function (method, url, ...args) {
|
|
230
376
|
this._requestUrl = typeof url === "string" && url.length > 0 ? url : null;
|
|
377
|
+
this._requestMethod = method;
|
|
378
|
+
this._capturedRequestHeaders = {};
|
|
231
379
|
return originalOpen.apply(this, [method, url, ...args]);
|
|
232
380
|
};
|
|
233
381
|
// Intercept send()
|
|
@@ -235,37 +383,112 @@ function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo, domainsTo
|
|
|
235
383
|
const url = this._requestUrl;
|
|
236
384
|
if (!url)
|
|
237
385
|
return originalSend.apply(this, args);
|
|
386
|
+
const domain = new URL(url).hostname;
|
|
238
387
|
// Skip domain check for excluded domains
|
|
239
|
-
if (
|
|
388
|
+
if (shouldSkipHeadersPropagation(url, domainsToNotPropagateHeaderTo)) {
|
|
240
389
|
return originalSend.apply(this, args);
|
|
241
390
|
}
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
391
|
+
const pageVisitUUID = sessionStorage.getItem("pageVisitUUID");
|
|
392
|
+
const networkUUID = uuidv4();
|
|
393
|
+
const xSf3RidHeaderValue = `${sessionId}/${pageVisitUUID}/${networkUUID}`;
|
|
394
|
+
const proceedSend = () => {
|
|
395
|
+
if (sessionId) {
|
|
396
|
+
try {
|
|
397
|
+
this.setRequestHeader(xSf3RidHeader, xSf3RidHeaderValue);
|
|
398
|
+
}
|
|
399
|
+
catch (e) {
|
|
400
|
+
console.warn(`Could not set X-Sf3-Rid header for ${url}`, e);
|
|
401
|
+
}
|
|
253
402
|
}
|
|
403
|
+
// On CORS or network error during send, add domain to passed-hosts list
|
|
404
|
+
this.addEventListener("error", () => {
|
|
405
|
+
dynamicExcludedHosts.add(domain);
|
|
406
|
+
}, { once: true });
|
|
407
|
+
// On successful send (HTTP 2xx), add domain to passed-hosts list
|
|
408
|
+
this.addEventListener("load", () => {
|
|
409
|
+
if (this.status === 0) {
|
|
410
|
+
// status 0 on load is often a CORS‐blocked response
|
|
411
|
+
dynamicExcludedHosts.add(domain);
|
|
412
|
+
}
|
|
413
|
+
if (this.status >= 200 && this.status < 300) {
|
|
414
|
+
dynamicPassedHosts.add(domain);
|
|
415
|
+
}
|
|
416
|
+
}, { once: true });
|
|
417
|
+
return originalSend.apply(this, args);
|
|
418
|
+
};
|
|
419
|
+
if (!dynamicPassedHosts.has(domain)) {
|
|
420
|
+
// perform XHR-based preflight
|
|
421
|
+
const init = {
|
|
422
|
+
method: this._requestMethod,
|
|
423
|
+
headers: this._capturedRequestHeaders,
|
|
424
|
+
credentials: this.withCredentials ? INCLUDE : SAME_ORIGIN,
|
|
425
|
+
};
|
|
426
|
+
performOptionsPreflightForXHR(url, init, xSf3RidHeaderValue, domain)
|
|
427
|
+
.then((preflight) => {
|
|
428
|
+
if (preflight === ActionType.IGNORE) {
|
|
429
|
+
dynamicExcludedHosts.add(domain);
|
|
430
|
+
originalSend.call(this, args);
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
proceedSend();
|
|
434
|
+
}
|
|
435
|
+
})
|
|
436
|
+
.catch(() => {
|
|
437
|
+
// On error, treat as ignore
|
|
438
|
+
dynamicExcludedHosts.add(domain);
|
|
439
|
+
originalSend.call(this, args);
|
|
440
|
+
});
|
|
441
|
+
// just return void
|
|
442
|
+
return;
|
|
254
443
|
}
|
|
255
|
-
return
|
|
444
|
+
return proceedSend();
|
|
256
445
|
};
|
|
257
446
|
}
|
|
447
|
+
/**
|
|
448
|
+
* Performs an OPTIONS preflight check to decide header propagation.
|
|
449
|
+
* Returns 'propagate' if OPTIONS succeeds, 'ignore' otherwise.
|
|
450
|
+
*/
|
|
451
|
+
async function performOptionsPreflight(target, thisArg, url, init, sessionId, domain) {
|
|
452
|
+
try {
|
|
453
|
+
const headers = new Headers(init.headers || {});
|
|
454
|
+
headers.set(xSf3RidHeader, sessionId);
|
|
455
|
+
const response = await target.call(thisArg, url, {
|
|
456
|
+
method: "OPTIONS",
|
|
457
|
+
headers,
|
|
458
|
+
});
|
|
459
|
+
if (response.ok) {
|
|
460
|
+
return ActionType.PROPAGATE;
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
DEBUG &&
|
|
464
|
+
console.log(`[Fetch Interceptor] OPTIONS returned status ${response.status} for ${domain}`);
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
// Treat fetch errors (likely CORS failures) as ignore
|
|
470
|
+
if (error instanceof TypeError || error?.message?.includes(CORS_KEYWORD)) {
|
|
471
|
+
DEBUG &&
|
|
472
|
+
console.log(`[Fetch Interceptor] Preflight OPTIONS CORS error for ${domain}:`, error);
|
|
473
|
+
return ActionType.IGNORE;
|
|
474
|
+
}
|
|
475
|
+
// Other failures also ignored as some APIs or reverse proxies (e.g. NGINX) don’t route
|
|
476
|
+
// or handle OPTIONS requests, leading to:
|
|
477
|
+
// * 404 Not Found
|
|
478
|
+
// * 405 Method Not Allowed
|
|
479
|
+
// * 500 Internal Server Error
|
|
480
|
+
DEBUG &&
|
|
481
|
+
console.log(`[Fetch Interceptor] Preflight OPTIONS failed for ${domain}:`, error);
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
258
485
|
// Updated fetch interceptor with exclusion handling
|
|
259
|
-
function setupFetchInterceptor(domainsToNotPropagateHeadersTo
|
|
486
|
+
function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
260
487
|
const originalFetch = window.fetch;
|
|
261
488
|
const sessionId = getOrSetSessionId();
|
|
262
|
-
const combinedIgnoreDomains = [
|
|
263
|
-
...DOMAINS_TO_NOT_PROPAGATE_HEADER_TO_DEFAULT,
|
|
264
|
-
...domainsToNotPropagateHeadersTo,
|
|
265
|
-
];
|
|
266
489
|
const cache = new Map();
|
|
267
490
|
window.fetch = new Proxy(originalFetch, {
|
|
268
|
-
apply: (target, thisArg, args) => {
|
|
491
|
+
apply: async (target, thisArg, args) => {
|
|
269
492
|
let input = args[0];
|
|
270
493
|
let init = args[1] || {};
|
|
271
494
|
let url;
|
|
@@ -281,30 +504,41 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo, domainsToPropagat
|
|
|
281
504
|
else {
|
|
282
505
|
return target.apply(thisArg, args); // Skip unsupported inputs
|
|
283
506
|
}
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
507
|
+
// Determine the target domain
|
|
508
|
+
const domain = new URL(url, window.location.href).hostname;
|
|
509
|
+
// Use cached decision if available
|
|
510
|
+
if (cache.has(domain)) {
|
|
511
|
+
const decision = cache.get(domain);
|
|
512
|
+
if (decision === ActionType.IGNORE) {
|
|
288
513
|
return target.apply(thisArg, args);
|
|
289
514
|
}
|
|
290
|
-
if (
|
|
515
|
+
if (decision === ActionType.PROPAGATE) {
|
|
291
516
|
return injectHeaderWrapper(target, thisArg, args, input, init, sessionId, url);
|
|
292
517
|
}
|
|
293
518
|
}
|
|
294
|
-
// Check
|
|
295
|
-
if (
|
|
296
|
-
cache.set(
|
|
519
|
+
// Check exclusion domains and cache 'ignore'
|
|
520
|
+
if (shouldSkipHeadersPropagation(url, domainsToNotPropagateHeadersTo)) {
|
|
521
|
+
cache.set(domain, ActionType.IGNORE);
|
|
297
522
|
return target.apply(thisArg, args);
|
|
298
523
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
524
|
+
let decision = ActionType.PROPAGATE;
|
|
525
|
+
// Check if domain verified before
|
|
526
|
+
if (!dynamicPassedHosts.has(domain)) {
|
|
527
|
+
// Perform OPTIONS preflight to decide header propagation
|
|
528
|
+
const res = await performOptionsPreflight(target, thisArg, url, init, sessionId, domain);
|
|
529
|
+
// Skip the header propagation as OPTIONS return Ignore
|
|
530
|
+
if (res === ActionType.IGNORE) {
|
|
531
|
+
decision = res;
|
|
532
|
+
dynamicExcludedHosts.add(domain);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
cache.set(domain, decision);
|
|
536
|
+
if (decision === ActionType.PROPAGATE) {
|
|
537
|
+
return injectHeaderWrapper(target, thisArg, args, input, init, sessionId, url);
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
304
540
|
return target.apply(thisArg, args);
|
|
305
541
|
}
|
|
306
|
-
cache.set(url, "propagate");
|
|
307
|
-
return injectHeaderWrapper(target, thisArg, args, input, init, sessionId, url);
|
|
308
542
|
},
|
|
309
543
|
});
|
|
310
544
|
// Wrapper function to emit 'networkRequest' event
|
|
@@ -314,12 +548,24 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo, domainsToPropagat
|
|
|
314
548
|
const urlAndStoredUuids = getUrlAndStoredUuids();
|
|
315
549
|
const method = init.method || "GET";
|
|
316
550
|
const startTime = Date.now();
|
|
551
|
+
const domain = new URL(url).hostname;
|
|
317
552
|
try {
|
|
318
|
-
|
|
553
|
+
let response = await injectHeader(target, thisArg, input, init, sessionId, urlAndStoredUuids.page_visit_uuid, networkUUID);
|
|
554
|
+
let isRetry = false;
|
|
555
|
+
// Retry logic for 400/403 before logging finished event
|
|
556
|
+
// If the server rejects our header, retry without it
|
|
557
|
+
if (BAD_HTTP_STATUS.includes(response.status)) {
|
|
558
|
+
DEBUG && console.log("Perform retry as status was fail:", response);
|
|
559
|
+
response = retryWithoutPropagateHeaders(target, thisArg, args, url);
|
|
560
|
+
isRetry = true;
|
|
561
|
+
}
|
|
319
562
|
const endTime = Date.now();
|
|
320
563
|
const status = response.status;
|
|
321
564
|
const success = response.ok;
|
|
322
|
-
const error = success ?
|
|
565
|
+
const error = success ? "" : `Request Error: ${response.statusText}`;
|
|
566
|
+
if (success) {
|
|
567
|
+
(isRetry ? dynamicExcludedHosts : dynamicPassedHosts).add(domain);
|
|
568
|
+
}
|
|
323
569
|
// Emit 'networkRequestFinished' event
|
|
324
570
|
const eventData = {
|
|
325
571
|
type: NetworkRequestEventId,
|
|
@@ -346,6 +592,16 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo, domainsToPropagat
|
|
|
346
592
|
const success = false;
|
|
347
593
|
const responseCode = error.response?.status || 500;
|
|
348
594
|
const errorMessage = error.message || "Fetch request failed";
|
|
595
|
+
// Treat fetch errors (likely CORS failures) as ignore
|
|
596
|
+
// Since some APIs or reverse proxies (such as NGINX) do not route or handle OPTIONS requests, CORS may occur while the request is being made.
|
|
597
|
+
if (error instanceof TypeError ||
|
|
598
|
+
error?.message?.includes(CORS_KEYWORD)) {
|
|
599
|
+
DEBUG &&
|
|
600
|
+
console.log(`[Fetch Interceptor] CORS error for ${domain}:`, error);
|
|
601
|
+
dynamicExcludedHosts.add(domain);
|
|
602
|
+
return target.apply(thisArg, args);
|
|
603
|
+
}
|
|
604
|
+
dynamicPassedHosts.add(domain);
|
|
349
605
|
// Emit 'networkRequestFinished' event with error
|
|
350
606
|
const eventData = {
|
|
351
607
|
type: NetworkRequestEventId,
|
|
@@ -362,7 +618,7 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo, domainsToPropagat
|
|
|
362
618
|
method,
|
|
363
619
|
url,
|
|
364
620
|
},
|
|
365
|
-
...urlAndStoredUuids
|
|
621
|
+
...urlAndStoredUuids,
|
|
366
622
|
};
|
|
367
623
|
sendEvent(eventData);
|
|
368
624
|
throw error;
|
|
@@ -393,6 +649,32 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo, domainsToPropagat
|
|
|
393
649
|
return await target.call(thisArg, input, modifiedInit);
|
|
394
650
|
}
|
|
395
651
|
}
|
|
652
|
+
// Helper to retry a fetch without the X-Sf3-Rid header if the initial attempt fails due to that header
|
|
653
|
+
async function retryWithoutPropagateHeaders(target, thisArg, args, url) {
|
|
654
|
+
const domain = new URL(url).hostname;
|
|
655
|
+
try {
|
|
656
|
+
// Retry the fetch without the header
|
|
657
|
+
// const retryResponse = await originalFetch(retryRequest);
|
|
658
|
+
const response = target.apply(thisArg, args);
|
|
659
|
+
// Check if retry succeeded (no network error thrown, and not a 400/403 response)
|
|
660
|
+
if (response.ok || !BAD_HTTP_STATUS.includes(response.status)) {
|
|
661
|
+
// Mark this domain to exclude the header going forward without header
|
|
662
|
+
dynamicExcludedHosts.add(domain);
|
|
663
|
+
cache.set(domain, ActionType.IGNORE); // mark domain as 'ignore' in the cache
|
|
664
|
+
// Log the original failure and the successful retry
|
|
665
|
+
console.info(`Retried request to ${url} without ${xSf3RidHeader} succeeded. ` +
|
|
666
|
+
`Added "${domain}" to header exclusion lists.`);
|
|
667
|
+
}
|
|
668
|
+
// Return the response from the retry attempt (successful or not)
|
|
669
|
+
return response;
|
|
670
|
+
}
|
|
671
|
+
catch (retryError) {
|
|
672
|
+
// Propagate the failure (no domain added to exclude lists since retry failed)
|
|
673
|
+
DEBUG &&
|
|
674
|
+
console.log(`Retry without ${xSf3RidHeader} for ${url} also failed:`, retryError);
|
|
675
|
+
throw retryError;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
396
678
|
}
|
|
397
679
|
// Main Recording Function
|
|
398
680
|
export async function startRecording({ apiKey, backendApi = "https://api-service.sailfishqa.com", domainsToPropagateHeaderTo = [], domainsToNotPropagateHeaderTo = [], serviceVersion = "", serviceIdentifier = "", }) {
|
|
@@ -401,13 +683,27 @@ export async function startRecording({ apiKey, backendApi = "https://api-service
|
|
|
401
683
|
initializeConsolePlugin(DEFAULT_CONSOLE_RECORDING_SETTINGS, sessionId);
|
|
402
684
|
storeCredentialsAndConnection({ apiKey, backendApi });
|
|
403
685
|
trackDomainChanges();
|
|
686
|
+
// Add provided domainsToNotPropagateHeaderTo to dynamicExcludedHosts without triggering updateExcludedHostsStorageAndBackend
|
|
687
|
+
domainsToNotPropagateHeaderTo?.forEach((host) => {
|
|
688
|
+
host?.trim() && originalExcludedAdd.call(dynamicExcludedHosts, host);
|
|
689
|
+
});
|
|
690
|
+
// Persist updated excluded hosts to localStorage
|
|
691
|
+
localStorage.setItem(DYNAMIC_EXCLUDED_HOSTS_KEY, JSON.stringify(Array.from(dynamicExcludedHosts)));
|
|
692
|
+
// Add provided domainsToPropagateHeaderTo to dynamicPassedHosts
|
|
693
|
+
domainsToPropagateHeaderTo.forEach((host) => {
|
|
694
|
+
originalPassedAdd.call(dynamicPassedHosts, host);
|
|
695
|
+
});
|
|
696
|
+
// Persist updated included hosts to localStorage
|
|
697
|
+
localStorage.setItem(DYNAMIC_PASSED_HOSTS_KEY, JSON.stringify(Array.from(dynamicPassedHosts)));
|
|
404
698
|
// Non-blocking GraphQL request to send the domains if provided
|
|
405
|
-
if (
|
|
406
|
-
sendDomainsToNotPropagateHeaderTo(apiKey,
|
|
699
|
+
if (dynamicExcludedHosts.size > 0) {
|
|
700
|
+
sendDomainsToNotPropagateHeaderTo(apiKey, [...dynamicExcludedHosts, ...DOMAINS_TO_NOT_PROPAGATE_HEADER_TO_DEFAULT], backendApi).catch((error) => console.error("Failed to send domains to not propagate header to:", error));
|
|
407
701
|
}
|
|
702
|
+
sessionStorage.setItem(SF_API_KEY_FOR_UPDATE, apiKey);
|
|
703
|
+
sessionStorage.setItem(SF_BACKEND_API, backendApi);
|
|
408
704
|
// Setup interceptors with custom ignore and propagate domains
|
|
409
|
-
setupXMLHttpRequestInterceptor(
|
|
410
|
-
setupFetchInterceptor(
|
|
705
|
+
setupXMLHttpRequestInterceptor(domainsToPropagateHeaderTo);
|
|
706
|
+
setupFetchInterceptor(domainsToPropagateHeaderTo);
|
|
411
707
|
gatherAndCacheDeviceInfo();
|
|
412
708
|
try {
|
|
413
709
|
const captureSettingsResponse = await fetchCaptureSettings(apiKey, backendApi);
|
|
@@ -438,9 +734,9 @@ export const initRecorder = async (options) => {
|
|
|
438
734
|
return startRecording(options);
|
|
439
735
|
};
|
|
440
736
|
// Re-export from other modules
|
|
441
|
-
export * from "./utils";
|
|
442
737
|
export * from "./graphql";
|
|
443
738
|
export * from "./recording";
|
|
444
739
|
export * from "./sendSailfishMessages";
|
|
445
740
|
export * from "./types";
|
|
741
|
+
export * from "./utils";
|
|
446
742
|
export * from "./websocket";
|