@sailfish-ai/recorder 1.7.41 → 1.7.46
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/constants.js +2 -0
- package/dist/exponentialBackoff.js +1 -0
- package/dist/inAppReportIssueModal.js +106 -43
- package/dist/index.js +56 -84
- 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/inAppReportIssueModal.d.ts +24 -1
- package/dist/types/index.d.ts +3 -2
- package/package.json +2 -2
package/dist/constants.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const DomContentEventId = 24;
|
|
2
2
|
export const NetworkRequestEventId = 27;
|
|
3
3
|
export const xSf3RidHeader = "X-Sf3-Rid";
|
|
4
|
+
// Values for DomContentLoadedEvent
|
|
4
5
|
export const DomContentSource = {
|
|
5
6
|
loading: 0,
|
|
6
7
|
contentLoaded: 1,
|
|
@@ -8,6 +9,7 @@ export const DomContentSource = {
|
|
|
8
9
|
beforeunload: 3,
|
|
9
10
|
unload: 4,
|
|
10
11
|
};
|
|
12
|
+
// Values for NetworkRequestEvent
|
|
11
13
|
export const Loading = "loading";
|
|
12
14
|
export const Complete = "complete";
|
|
13
15
|
export const STATIC_EXTENSIONS = [
|
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
import { createTriageFromRecorder } from "./graphql";
|
|
2
|
+
// TODO - enable configuration by keyboard typing in UI
|
|
3
|
+
const DEFAULT_SHORTCUTS = {
|
|
4
|
+
enabled: false,
|
|
5
|
+
openModalExistingMode: { key: "e", requireCmdCtrl: false },
|
|
6
|
+
openModalCaptureNewMode: { key: "n", requireCmdCtrl: false },
|
|
7
|
+
closeModal: { key: "escape", requireCmdCtrl: false },
|
|
8
|
+
submitReport: { key: "enter", requireCmdCtrl: true },
|
|
9
|
+
startRecording: { key: "r", requireCmdCtrl: false },
|
|
10
|
+
stopRecording: { key: "escape", requireCmdCtrl: true },
|
|
11
|
+
};
|
|
12
|
+
export const ReportIssueContext = {
|
|
13
|
+
shortcuts: { ...DEFAULT_SHORTCUTS },
|
|
14
|
+
resolveSessionId: null,
|
|
15
|
+
apiKey: null,
|
|
16
|
+
backendApi: null,
|
|
17
|
+
triageBaseUrl: "https://app.sailfishqa.com",
|
|
18
|
+
};
|
|
2
19
|
let modalEl = null;
|
|
3
20
|
let currentState = {
|
|
4
21
|
mode: "lookback",
|
|
@@ -9,12 +26,6 @@ let recordingStartTime = null;
|
|
|
9
26
|
let recordingEndTime = null;
|
|
10
27
|
let timerInterval = null;
|
|
11
28
|
let isRecording = false;
|
|
12
|
-
export const ReportIssueContext = {
|
|
13
|
-
resolveSessionId: null,
|
|
14
|
-
apiKey: null,
|
|
15
|
-
backendApi: null,
|
|
16
|
-
triageBaseUrl: "https://app.sailfishqa.com",
|
|
17
|
-
};
|
|
18
29
|
function isMacPlatform() {
|
|
19
30
|
// Newer API (Chrome, Edge, Opera)
|
|
20
31
|
const uaData = navigator.userAgentData;
|
|
@@ -33,62 +44,118 @@ function isTypingInInput() {
|
|
|
33
44
|
el instanceof HTMLTextAreaElement ||
|
|
34
45
|
(el instanceof HTMLElement && el.isContentEditable));
|
|
35
46
|
}
|
|
47
|
+
function formatShortcutKeyLabel(key) {
|
|
48
|
+
const map = {
|
|
49
|
+
escape: "esc",
|
|
50
|
+
};
|
|
51
|
+
return (map[key.toLowerCase()] || key).toUpperCase();
|
|
52
|
+
}
|
|
53
|
+
function getShortcutLabel(config) {
|
|
54
|
+
const parts = [];
|
|
55
|
+
if (config.requireCmdCtrl) {
|
|
56
|
+
parts.push(getShortcutKeyCmdCtrlLabel());
|
|
57
|
+
}
|
|
58
|
+
parts.push(formatShortcutKeyLabel(config.key));
|
|
59
|
+
return parts
|
|
60
|
+
.map((p) => `<span style="background: #F1F5F9; border:1px solid #cbd5e1; border-radius: 4px; padding: 0 4px; font-weight: 500; font-size: 12px; color: #94A3B8; line-height: 16px;">${p}</span>`)
|
|
61
|
+
.join(config.requireCmdCtrl ? " + " : "");
|
|
62
|
+
}
|
|
63
|
+
function mergeShortcutsConfig(userConfig) {
|
|
64
|
+
const merged = { ...DEFAULT_SHORTCUTS };
|
|
65
|
+
if (!userConfig)
|
|
66
|
+
return merged;
|
|
67
|
+
// Handle boolean key separately
|
|
68
|
+
if (typeof userConfig.enabled === "boolean") {
|
|
69
|
+
merged.enabled = userConfig.enabled;
|
|
70
|
+
}
|
|
71
|
+
// Handle object keys explicitly
|
|
72
|
+
const objectKeys = [
|
|
73
|
+
"openModalExistingMode",
|
|
74
|
+
"openModalCaptureNewMode",
|
|
75
|
+
"closeModal",
|
|
76
|
+
"submitReport",
|
|
77
|
+
"startRecording",
|
|
78
|
+
"stopRecording",
|
|
79
|
+
];
|
|
80
|
+
for (const k of objectKeys) {
|
|
81
|
+
const val = userConfig[k];
|
|
82
|
+
if (val && typeof val === "object") {
|
|
83
|
+
merged[k] = { ...merged[k], ...val };
|
|
84
|
+
merged[k].key = merged[k].key.toLowerCase();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return merged;
|
|
88
|
+
}
|
|
89
|
+
function getShortcutLabelFromContext(shortcutKey) {
|
|
90
|
+
const config = ReportIssueContext.shortcuts[shortcutKey];
|
|
91
|
+
return getShortcutLabel(config);
|
|
92
|
+
}
|
|
36
93
|
export function setupIssueReporting(options) {
|
|
37
94
|
ReportIssueContext.apiKey = options.apiKey;
|
|
38
95
|
ReportIssueContext.backendApi = options.backendApi;
|
|
39
96
|
ReportIssueContext.resolveSessionId = options.getSessionId;
|
|
40
|
-
|
|
97
|
+
ReportIssueContext.shortcuts = mergeShortcutsConfig(options.shortcuts);
|
|
98
|
+
const { shortcuts } = ReportIssueContext;
|
|
41
99
|
window.addEventListener("keydown", (e) => {
|
|
42
100
|
const typingInInput = isTypingInInput();
|
|
43
101
|
const key = e.key.toLowerCase();
|
|
44
102
|
const isCmdOrCtrl = e.metaKey || e.ctrlKey;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
e.preventDefault();
|
|
54
|
-
injectModalHTML("startnow");
|
|
55
|
-
return;
|
|
103
|
+
const modalOpen = !!document.getElementById("sf-report-issue-modal");
|
|
104
|
+
const enableModeShortcuts = !typingInInput && (shortcuts.enabled || modalOpen);
|
|
105
|
+
const shortcutUsed = (shortcutKey) => key === shortcuts[shortcutKey].key &&
|
|
106
|
+
isCmdOrCtrl === shortcuts[shortcutKey].requireCmdCtrl;
|
|
107
|
+
const openModal = modalOpen
|
|
108
|
+
? (mode) => {
|
|
109
|
+
setActiveTab(mode);
|
|
110
|
+
updateModeSpecificUI(mode);
|
|
56
111
|
}
|
|
112
|
+
: injectModalHTML;
|
|
113
|
+
// --- Open modal OR switch mode ---
|
|
114
|
+
if (enableModeShortcuts && shortcutUsed("openModalExistingMode")) {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
openModal("lookback");
|
|
117
|
+
return;
|
|
57
118
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
119
|
+
if (enableModeShortcuts && shortcutUsed("openModalCaptureNewMode")) {
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
openModal("startnow");
|
|
61
122
|
return;
|
|
62
|
-
|
|
63
|
-
|
|
123
|
+
}
|
|
124
|
+
// --- Close modal ---
|
|
125
|
+
if (modalOpen && !isRecording && shortcutUsed("closeModal")) {
|
|
64
126
|
e.preventDefault();
|
|
65
127
|
closeModal();
|
|
66
128
|
return;
|
|
67
129
|
}
|
|
68
|
-
//
|
|
69
|
-
if (
|
|
130
|
+
// --- Submit report ---
|
|
131
|
+
if (modalOpen && shortcutUsed("submitReport")) {
|
|
70
132
|
const submitBtn = document.getElementById("sf-issue-submit-btn");
|
|
71
133
|
if (submitBtn && !submitBtn.disabled) {
|
|
72
134
|
e.preventDefault();
|
|
73
135
|
submitBtn.click();
|
|
74
136
|
}
|
|
137
|
+
return;
|
|
75
138
|
}
|
|
76
|
-
//
|
|
77
|
-
if (
|
|
139
|
+
// --- Stop recording ---
|
|
140
|
+
if (isRecording &&
|
|
141
|
+
key === shortcuts.stopRecording.key &&
|
|
142
|
+
isCmdOrCtrl === shortcuts.stopRecording.requireCmdCtrl) {
|
|
78
143
|
e.preventDefault();
|
|
79
144
|
stopRecording();
|
|
145
|
+
return;
|
|
80
146
|
}
|
|
81
|
-
//
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
key ===
|
|
85
|
-
|
|
86
|
-
|
|
147
|
+
// --- Start recording ---
|
|
148
|
+
if (modalOpen &&
|
|
149
|
+
currentState.mode === "startnow" &&
|
|
150
|
+
key === shortcuts.startRecording.key &&
|
|
151
|
+
isCmdOrCtrl === shortcuts.startRecording.requireCmdCtrl &&
|
|
152
|
+
!typingInInput) {
|
|
87
153
|
const recordBtn = document.getElementById("sf-start-recording-btn");
|
|
88
154
|
if (recordBtn) {
|
|
89
155
|
e.preventDefault();
|
|
90
156
|
recordBtn.click();
|
|
91
157
|
}
|
|
158
|
+
return;
|
|
92
159
|
}
|
|
93
160
|
});
|
|
94
161
|
}
|
|
@@ -159,11 +226,11 @@ function injectModalHTML(initialMode = "lookback") {
|
|
|
159
226
|
<div id="sf-issue-tabs" style="display:flex; gap:4px; margin-bottom:16px; background:#f1f5f9; padding:6px; border-radius:6px; width: fit-content;">
|
|
160
227
|
<button id="sf-tab-lookback" data-mode="lookback" class="sf-issue-tab ${!isStartNow ? "active" : ""}"
|
|
161
228
|
style="padding:6px 12px; border:none; background:${!isStartNow ? "white" : "transparent"}; color: ${!isStartNow ? "#0F172A" : "#64748B"}; border-radius:4px; font-size:14px; cursor:pointer; font-weight:500; height:32px; display: flex; align-items: center; gap: 8px;">
|
|
162
|
-
Existing
|
|
229
|
+
Existing ${getShortcutLabelFromContext("openModalExistingMode")}
|
|
163
230
|
</button>
|
|
164
231
|
<button id="sf-tab-startnow" data-mode="startnow" class="sf-issue-tab ${isStartNow ? "active" : ""}"
|
|
165
232
|
style="padding:6px 12px; border:none; background:${isStartNow ? "white" : "transparent"}; color: ${isStartNow ? "#0F172A" : "#64748B"}; border-radius:4px; font-size:14px; cursor:pointer; font-weight:500; height:32px; display: flex; align-items: center; gap: 8px;">
|
|
166
|
-
Capture new
|
|
233
|
+
Capture new ${getShortcutLabelFromContext("openModalCaptureNewMode")}
|
|
167
234
|
</button>
|
|
168
235
|
</div>
|
|
169
236
|
|
|
@@ -189,7 +256,7 @@ function injectModalHTML(initialMode = "lookback") {
|
|
|
189
256
|
<textarea id="sf-issue-description" placeholder="Add description here"
|
|
190
257
|
style="width:100%; height:80px; padding:8px 12px; font-size:14px;
|
|
191
258
|
border:1px solid #cbd5e1; border-radius:6px; margin-bottom:20px;
|
|
192
|
-
resize:none; outline:none;"
|
|
259
|
+
resize:none; outline:none;">${currentState.description}</textarea>
|
|
193
260
|
|
|
194
261
|
<div id="sf-lookback-container" style="display:${isStartNow ? "none" : "block"}; margin-bottom:20px;">
|
|
195
262
|
<label for="sf-lookback-minutes" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
|
|
@@ -261,7 +328,7 @@ function injectModalHTML(initialMode = "lookback") {
|
|
|
261
328
|
<div style="width: 14px; height: 14px; background: #fc5555; border-radius: 50%; border: 1px solid #991b1b;"></div>
|
|
262
329
|
</div>
|
|
263
330
|
<span>Start Recording</span>
|
|
264
|
-
|
|
331
|
+
${getShortcutLabelFromContext("startRecording")}
|
|
265
332
|
</button>
|
|
266
333
|
</div>
|
|
267
334
|
|
|
@@ -270,9 +337,7 @@ function injectModalHTML(initialMode = "lookback") {
|
|
|
270
337
|
border-radius:6px; font-size:14px; line-height: 24px; font-weight:500;
|
|
271
338
|
cursor:${isStartNow ? "not-allowed" : "pointer"}; opacity:${isStartNow ? "0.4" : "1"}; margin-bottom:1px" ${isStartNow ? "disabled" : ""}>
|
|
272
339
|
Report <span style="margin-left: 8px; display: inline-flex; align-items: center; gap: 4px; color: #94A3B8; font-size: 12px; line-height:16px;">
|
|
273
|
-
|
|
274
|
-
+
|
|
275
|
-
<span style="background: #F1F5F9; border-radius: 4px; padding: 0 4px; font-weight: 500;">ENTER</span>
|
|
340
|
+
${getShortcutLabelFromContext("submitReport")}
|
|
276
341
|
</span>
|
|
277
342
|
</button>
|
|
278
343
|
</div>
|
|
@@ -487,9 +552,7 @@ function showFloatingTimer() {
|
|
|
487
552
|
<div style="display:flex; align-items:center; gap:4px; margin-left:auto; font-size:14px; color:#0F172A;">
|
|
488
553
|
<span style="color: #71717A;">stop</span>
|
|
489
554
|
<span style="display: inline-flex; align-items: center; gap: 4px; color: #94A3B8; font-size: 12px; line-height:16px;">
|
|
490
|
-
|
|
491
|
-
+
|
|
492
|
-
<span style="background: #F1F5F9; border-radius: 4px; border:1px solid #cbd5e1; padding: 0 4px; font-weight: 500;">ESC</span>
|
|
555
|
+
${getShortcutLabelFromContext("stopRecording")}
|
|
493
556
|
</span>
|
|
494
557
|
</div>
|
|
495
558
|
`;
|
package/dist/index.js
CHANGED
|
@@ -323,6 +323,7 @@ function trackDomainChanges() {
|
|
|
323
323
|
timestamp,
|
|
324
324
|
page_visit_uuid: pageVisitUUID,
|
|
325
325
|
prev_page_visit_uuid: prevPageVisitUUID,
|
|
326
|
+
session_id: getOrSetSessionId(),
|
|
326
327
|
},
|
|
327
328
|
});
|
|
328
329
|
}
|
|
@@ -422,22 +423,46 @@ function storeCredentialsAndConnection({ apiKey, backendApi, }) {
|
|
|
422
423
|
sessionStorage.setItem("sailfishBackendApi", backendApi);
|
|
423
424
|
}
|
|
424
425
|
// Utility function to match domains or paths with wildcard support
|
|
425
|
-
export function matchUrlWithWildcard(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
426
|
+
export function matchUrlWithWildcard(input, patterns) {
|
|
427
|
+
// Tolerate non-string inputs (Request, URL, etc.); coerce to string when possible.
|
|
428
|
+
let urlStr;
|
|
429
|
+
if (typeof input === "string") {
|
|
430
|
+
urlStr = input;
|
|
431
|
+
}
|
|
432
|
+
else if (typeof URL !== "undefined" && input instanceof URL) {
|
|
433
|
+
urlStr = input.href;
|
|
434
|
+
}
|
|
435
|
+
else if (typeof Request !== "undefined" && input instanceof Request) {
|
|
436
|
+
urlStr = input.url;
|
|
437
|
+
}
|
|
438
|
+
else if (input != null && typeof input.toString === "function") {
|
|
439
|
+
// As per web APIs, fetch/open may accept any object with a stringifier.
|
|
440
|
+
urlStr = input.toString();
|
|
441
|
+
}
|
|
442
|
+
if (!urlStr)
|
|
443
|
+
return false;
|
|
444
|
+
// Use WHATWG URL parsing with a base for relative URLs.
|
|
445
|
+
let parsed;
|
|
446
|
+
try {
|
|
447
|
+
const base = typeof window !== "undefined"
|
|
448
|
+
? window.location.href
|
|
449
|
+
: "http://localhost/";
|
|
450
|
+
parsed = new URL(urlStr, base);
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
return false; // If we can't parse, just say "no match" instead of throwing.
|
|
454
|
+
}
|
|
455
|
+
const { hostname, pathname, port, protocol } = parsed;
|
|
456
|
+
// Only match http(s) hosts/paths; ignore others like data:, blob:, about:.
|
|
457
|
+
if (!/^https?:$/.test(protocol))
|
|
458
|
+
return false;
|
|
434
459
|
// Handle stripping 'www.' and port
|
|
435
460
|
const domain = hostname.startsWith("www.")
|
|
436
461
|
? hostname.slice(4).toLowerCase()
|
|
437
462
|
: hostname.toLowerCase();
|
|
438
463
|
return patterns.some((pattern) => {
|
|
439
464
|
// Strip any protocol and handle port in the pattern if present
|
|
440
|
-
const strippedPattern = pattern.replace(/^[a-zA-Z]+:\/\//, "");
|
|
465
|
+
const strippedPattern = String(pattern || "").replace(/^[a-zA-Z]+:\/\//, "");
|
|
441
466
|
let [patternDomain, patternPath] = strippedPattern.split("/", 2);
|
|
442
467
|
// Handle port in pattern
|
|
443
468
|
let patternPort = "";
|
|
@@ -453,14 +478,16 @@ export function matchUrlWithWildcard(url, patterns) {
|
|
|
453
478
|
// Strip 'www.' from both the input domain and the pattern domain for comparison
|
|
454
479
|
const strippedDomain = domain.startsWith("www.") ? domain.slice(4) : domain;
|
|
455
480
|
// If pattern specifies a port, match the exact port
|
|
456
|
-
if (patternPort && port !== patternPort)
|
|
481
|
+
if (patternPort && port !== patternPort)
|
|
457
482
|
return false;
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
strippedDomain ===
|
|
463
|
-
|
|
483
|
+
// handle patterns like "*.example.com"
|
|
484
|
+
if (patternDomain.startsWith("*.")) {
|
|
485
|
+
const base = patternDomain.slice(2).toLowerCase();
|
|
486
|
+
const isDomainMatch = domain === base ||
|
|
487
|
+
strippedDomain === base ||
|
|
488
|
+
domain.endsWith("." + base);
|
|
489
|
+
if (!isDomainMatch)
|
|
490
|
+
return false;
|
|
464
491
|
if (patternPath) {
|
|
465
492
|
const normalizedPatternPath = patternPath
|
|
466
493
|
.replace(/\*/g, ".*") // Replace '*' with regex to match any characters
|
|
@@ -471,9 +498,8 @@ export function matchUrlWithWildcard(url, patterns) {
|
|
|
471
498
|
return true; // Domain matched, no path required
|
|
472
499
|
}
|
|
473
500
|
// Check if the domain matches (include check for base domain without www)
|
|
474
|
-
if (!domainRegex.test(strippedDomain) && !domainRegex.test(domain))
|
|
501
|
+
if (!domainRegex.test(strippedDomain) && !domainRegex.test(domain))
|
|
475
502
|
return false;
|
|
476
|
-
}
|
|
477
503
|
// If there's a path in the pattern, match the path
|
|
478
504
|
if (patternPath) {
|
|
479
505
|
const normalizedPatternPath = patternPath
|
|
@@ -487,7 +513,14 @@ export function matchUrlWithWildcard(url, patterns) {
|
|
|
487
513
|
});
|
|
488
514
|
}
|
|
489
515
|
function shouldSkipHeadersPropagation(url, domainsToNotPropagateHeaderTo = []) {
|
|
490
|
-
|
|
516
|
+
let urlObj;
|
|
517
|
+
try {
|
|
518
|
+
urlObj = new URL(typeof url === "string" ? url : String(url?.url ?? url), window.location.href);
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
// If we cannot parse, play it safe and do NOT inject headers.
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
491
524
|
// 1️⃣ STATIC ASSET EXCLUSIONS (by comprehensive file extension list)
|
|
492
525
|
for (const ext of STATIC_EXTENSIONS) {
|
|
493
526
|
if (urlObj.pathname.toLowerCase().endsWith(ext)) {
|
|
@@ -503,11 +536,6 @@ function shouldSkipHeadersPropagation(url, domainsToNotPropagateHeaderTo = []) {
|
|
|
503
536
|
if (matchUrlWithWildcard(url, combinedPatterns)) {
|
|
504
537
|
return true;
|
|
505
538
|
}
|
|
506
|
-
// 3️⃣ DYNAMIC-FAILURE EXCLUSION (exact host)
|
|
507
|
-
const hostname = urlObj.hostname;
|
|
508
|
-
if (dynamicExcludedHosts.has(hostname)) {
|
|
509
|
-
return true;
|
|
510
|
-
}
|
|
511
539
|
return false;
|
|
512
540
|
}
|
|
513
541
|
// Updated XMLHttpRequest interceptor with domain exclusion
|
|
@@ -540,10 +568,6 @@ function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo = []) {
|
|
|
540
568
|
if (!url) {
|
|
541
569
|
return originalSend.apply(this, args);
|
|
542
570
|
}
|
|
543
|
-
// parse host+path for exclusion
|
|
544
|
-
const urlObj = new URL(url, window.location.href);
|
|
545
|
-
const domain = urlObj.hostname;
|
|
546
|
-
const hostPath = urlObj.pathname === "/" ? domain : `${domain}${urlObj.pathname}`;
|
|
547
571
|
// 1️⃣ Skip injection for excluded domains/paths
|
|
548
572
|
if (shouldSkipHeadersPropagation(url, domainsToNotPropagateHeaderTo)) {
|
|
549
573
|
return originalSend.apply(this, args);
|
|
@@ -584,15 +608,6 @@ function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo = []) {
|
|
|
584
608
|
},
|
|
585
609
|
...getUrlAndStoredUuids(),
|
|
586
610
|
});
|
|
587
|
-
// 5️⃣ Update dynamic sets
|
|
588
|
-
if (success) {
|
|
589
|
-
// once any route passes, skip preflight on entire domain
|
|
590
|
-
dynamicPassedHosts.add(domain);
|
|
591
|
-
}
|
|
592
|
-
else {
|
|
593
|
-
// only exclude the specific failing path
|
|
594
|
-
dynamicExcludedHosts.add(hostPath);
|
|
595
|
-
}
|
|
596
611
|
};
|
|
597
612
|
// 6️⃣ On successful load
|
|
598
613
|
this.addEventListener("load", () => {
|
|
@@ -622,7 +637,6 @@ function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo = []) {
|
|
|
622
637
|
function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
623
638
|
const originalFetch = window.fetch;
|
|
624
639
|
const sessionId = getOrSetSessionId();
|
|
625
|
-
const cachedDomains = new Map();
|
|
626
640
|
window.fetch = new Proxy(originalFetch, {
|
|
627
641
|
apply: async (target, thisArg, args) => {
|
|
628
642
|
let input = args[0];
|
|
@@ -656,9 +670,6 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
656
670
|
if (!sessionId) {
|
|
657
671
|
return target.apply(thisArg, args);
|
|
658
672
|
}
|
|
659
|
-
const urlObj = new URL(url, window.location.href);
|
|
660
|
-
const domain = urlObj.hostname;
|
|
661
|
-
const hostPath = urlObj.pathname === "/" ? domain : `${domain}${urlObj.pathname}`;
|
|
662
673
|
const networkUUID = uuidv4();
|
|
663
674
|
const urlAndStoredUuids = getUrlAndStoredUuids();
|
|
664
675
|
const method = init.method || "GET";
|
|
@@ -676,13 +687,6 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
676
687
|
const status = response.status;
|
|
677
688
|
const success = response.ok;
|
|
678
689
|
const error = success ? "" : `Request Error: ${response.statusText}`;
|
|
679
|
-
// 5️⃣ Update dynamic sets
|
|
680
|
-
if (success) {
|
|
681
|
-
dynamicPassedHosts.add(domain);
|
|
682
|
-
}
|
|
683
|
-
else {
|
|
684
|
-
dynamicExcludedHosts.add(hostPath);
|
|
685
|
-
}
|
|
686
690
|
// Emit 'networkRequestFinished' event
|
|
687
691
|
const eventData = {
|
|
688
692
|
type: NetworkRequestEventId,
|
|
@@ -698,6 +702,7 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
698
702
|
error,
|
|
699
703
|
method,
|
|
700
704
|
url,
|
|
705
|
+
retry_without_trace_id: isRetry,
|
|
701
706
|
},
|
|
702
707
|
...urlAndStoredUuids,
|
|
703
708
|
};
|
|
@@ -712,11 +717,8 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
712
717
|
if (error instanceof TypeError &&
|
|
713
718
|
error?.message?.toLowerCase().includes(CORS_KEYWORD.toLowerCase())) {
|
|
714
719
|
// CORS/network failure: exclude just this path
|
|
715
|
-
dynamicExcludedHosts.add(hostPath);
|
|
716
720
|
return target.apply(thisArg, args);
|
|
717
721
|
}
|
|
718
|
-
// On other errors, treat as “passed” so we don’t re-preflight
|
|
719
|
-
dynamicPassedHosts.add(domain);
|
|
720
722
|
const eventData = {
|
|
721
723
|
type: NetworkRequestEventId,
|
|
722
724
|
timestamp: endTime,
|
|
@@ -761,7 +763,6 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
761
763
|
}
|
|
762
764
|
// Helper to retry a fetch without the X-Sf3-Rid header if the initial attempt fails due to that header
|
|
763
765
|
async function retryWithoutPropagateHeaders(target, thisArg, args, url) {
|
|
764
|
-
const domain = new URL(url).hostname;
|
|
765
766
|
try {
|
|
766
767
|
// **Fix:** Properly await and clone the request without the tracing header
|
|
767
768
|
let input = args[0];
|
|
@@ -772,12 +773,6 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
772
773
|
retryHeaders.delete(xSf3RidHeader);
|
|
773
774
|
retryInit.headers = retryHeaders;
|
|
774
775
|
const response = await target.call(thisArg, input, retryInit);
|
|
775
|
-
if (response.ok || !BAD_HTTP_STATUS.includes(response.status)) {
|
|
776
|
-
dynamicExcludedHosts.add(domain);
|
|
777
|
-
cachedDomains.set(domain, ActionType.IGNORE);
|
|
778
|
-
DEBUG &&
|
|
779
|
-
console.info(`Retried request to ${url} without ${xSf3RidHeader} succeeded. Added "${domain}" to exclusion list.`);
|
|
780
|
-
}
|
|
781
776
|
return response;
|
|
782
777
|
}
|
|
783
778
|
else if (input instanceof Request) {
|
|
@@ -787,12 +782,6 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
787
782
|
retryHeaders.delete(xSf3RidHeader);
|
|
788
783
|
const retryReq = new Request(cloned, { headers: retryHeaders });
|
|
789
784
|
const response = await target.call(thisArg, retryReq, init);
|
|
790
|
-
if (response.ok || !BAD_HTTP_STATUS.includes(response.status)) {
|
|
791
|
-
dynamicExcludedHosts.add(domain);
|
|
792
|
-
cachedDomains.set(domain, ActionType.IGNORE);
|
|
793
|
-
DEBUG &&
|
|
794
|
-
console.info(`Retried request to ${url} without ${xSf3RidHeader} succeeded. Added "${domain}" to exclusion list.`);
|
|
795
|
-
}
|
|
796
785
|
return response;
|
|
797
786
|
}
|
|
798
787
|
else {
|
|
@@ -865,24 +854,6 @@ export async function startRecording({ apiKey, backendApi = "https://api-service
|
|
|
865
854
|
initializeConsolePlugin(DEFAULT_CONSOLE_RECORDING_SETTINGS, sessionId);
|
|
866
855
|
storeCredentialsAndConnection({ apiKey, backendApi });
|
|
867
856
|
trackDomainChanges();
|
|
868
|
-
// ─── Merge stored excludes + passed-in excludes ───
|
|
869
|
-
const initialExcludes = new Set(dynamicExcludedHosts);
|
|
870
|
-
domainsToNotPropagateHeaderTo.forEach((host) => {
|
|
871
|
-
if (host?.trim()) {
|
|
872
|
-
dynamicExcludedHosts.add(host.trim());
|
|
873
|
-
}
|
|
874
|
-
});
|
|
875
|
-
const newlyExcluded = Array.from(dynamicExcludedHosts).filter((h) => !initialExcludes.has(h));
|
|
876
|
-
if (newlyExcluded.length) {
|
|
877
|
-
// single notify of the full updated list
|
|
878
|
-
updateExcludedHostsStorageAndBackend(dynamicExcludedHosts);
|
|
879
|
-
}
|
|
880
|
-
// ─── Merge passed hosts ───
|
|
881
|
-
domainsToPropagateHeaderTo.forEach((host) => {
|
|
882
|
-
if (host?.trim()) {
|
|
883
|
-
dynamicPassedHosts.add(host.trim());
|
|
884
|
-
}
|
|
885
|
-
});
|
|
886
857
|
sessionStorage.setItem(SF_API_KEY_FOR_UPDATE, apiKey);
|
|
887
858
|
sessionStorage.setItem(SF_BACKEND_API, backendApi);
|
|
888
859
|
// Setup interceptors with custom ignore and propagate domains
|
|
@@ -910,6 +881,7 @@ export async function startRecording({ apiKey, backendApi = "https://api-service
|
|
|
910
881
|
}
|
|
911
882
|
}
|
|
912
883
|
export const initRecorder = async (options) => {
|
|
884
|
+
console.log("Initializing Sailfish Recorder with options:", options);
|
|
913
885
|
// Only run on the client (browser) environment
|
|
914
886
|
if (typeof window === "undefined") {
|
|
915
887
|
return;
|
|
@@ -920,7 +892,7 @@ export const initRecorder = async (options) => {
|
|
|
920
892
|
apiKey: options.apiKey,
|
|
921
893
|
backendApi: options.backendApi ?? "https://api-service.sailfishqa.com",
|
|
922
894
|
getSessionId: () => getOrSetSessionId(),
|
|
923
|
-
|
|
895
|
+
shortcuts: options.reportIssueShortcuts,
|
|
924
896
|
});
|
|
925
897
|
});
|
|
926
898
|
};
|