@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 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 { initializeDomContentEvents, initializeConsolePlugin, initializeRecording, getUrlAndStoredUuids, } from "./recording";
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, domainsToPropagateHeadersTo = []) {
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
- // Combined ignore and propagate logic
224
- const combinedIgnoreDomains = [
225
- ...DOMAINS_TO_NOT_PROPAGATE_HEADER_TO_DEFAULT,
226
- ...domainsToNotPropagateHeaderTo,
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 (matchUrlWithWildcard(url, combinedIgnoreDomains)) {
388
+ if (shouldSkipHeadersPropagation(url, domainsToNotPropagateHeaderTo)) {
240
389
  return originalSend.apply(this, args);
241
390
  }
242
- // Check if domain should propagate headers
243
- const shouldPropagateHeader = domainsToPropagateHeadersTo.length === 0 ||
244
- matchUrlWithWildcard(url, domainsToPropagateHeadersTo);
245
- if (sessionId && shouldPropagateHeader) {
246
- try {
247
- const pageVisitUUID = sessionStorage.getItem("pageVisitUUID");
248
- const networkUUID = uuidv4();
249
- this.setRequestHeader(xSf3RidHeader, `${sessionId}/${pageVisitUUID}/${networkUUID}`);
250
- }
251
- catch (e) {
252
- console.warn(`Could not set X-Sf3-Rid header for ${url}`, e);
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 originalSend.apply(this, args);
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, domainsToPropagateHeadersTo = []) {
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
- // Cache check
285
- if (cache.has(url)) {
286
- const cachedResult = cache.get(url);
287
- if (cachedResult === "ignore") {
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 (cachedResult === "propagate") {
515
+ if (decision === ActionType.PROPAGATE) {
291
516
  return injectHeaderWrapper(target, thisArg, args, input, init, sessionId, url);
292
517
  }
293
518
  }
294
- // Check domain exclusion
295
- if (matchUrlWithWildcard(url, combinedIgnoreDomains)) {
296
- cache.set(url, "ignore");
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
- // Check domain propagation
300
- const shouldPropagateHeader = domainsToPropagateHeadersTo.length === 0 ||
301
- matchUrlWithWildcard(url, domainsToPropagateHeadersTo);
302
- if (!shouldPropagateHeader) {
303
- cache.set(url, "ignore");
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
- const response = await injectHeader(target, thisArg, input, init, sessionId, urlAndStoredUuids.page_visit_uuid, networkUUID);
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 ? '' : `Request Error: ${response.statusText}`;
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 (domainsToNotPropagateHeaderTo.length > 0) {
406
- sendDomainsToNotPropagateHeaderTo(apiKey, domainsToNotPropagateHeaderTo, backendApi).catch((error) => console.error("Failed to send domains to not propagate header to:", error));
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(domainsToNotPropagateHeaderTo, domainsToPropagateHeaderTo);
410
- setupFetchInterceptor(domainsToNotPropagateHeaderTo, domainsToPropagateHeaderTo);
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";