@sailfish-ai/recorder 1.7.11 → 1.7.12-alpha5

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