@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 +27 -0
- package/dist/index.js +356 -51
- package/dist/modal.js +628 -0
- 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/graphql.d.ts +2 -1
- package/dist/types/index.d.ts +5 -3
- package/dist/types/modal.d.ts +7 -0
- package/dist/types/types.d.ts +5 -0
- package/package.json +1 -1
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 {
|
|
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
|
|
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
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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 (
|
|
389
|
+
if (shouldSkipHeadersPropagation(url, domainsToNotPropagateHeaderTo)) {
|
|
240
390
|
return originalSend.apply(this, args);
|
|
241
391
|
}
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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 (
|
|
516
|
+
if (decision === ActionType.PROPAGATE) {
|
|
291
517
|
return injectHeaderWrapper(target, thisArg, args, input, init, sessionId, url);
|
|
292
518
|
}
|
|
293
519
|
}
|
|
294
|
-
// Check
|
|
295
|
-
if (
|
|
296
|
-
cache.set(
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
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 ?
|
|
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 (
|
|
406
|
-
sendDomainsToNotPropagateHeaderTo(apiKey,
|
|
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(
|
|
410
|
-
setupFetchInterceptor(
|
|
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";
|