@sailfish-ai/recorder 1.7.21 → 1.7.22
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 +60 -0
- package/dist/index.js +339 -354
- 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/constants.d.ts +1 -0
- package/dist/types/index.d.ts +12 -0
- package/package.json +12 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const DEBUG = import.meta.env.VITE_DEBUG ? import.meta.env.VITE_DEBUG : false;
|
|
2
2
|
// import { NetworkRecordOptions } from "@sailfish-rrweb/rrweb-plugin-network-record";
|
|
3
3
|
import { v4 as uuidv4 } from "uuid";
|
|
4
|
-
import { NetworkRequestEventId, xSf3RidHeader } from "./constants";
|
|
4
|
+
import { NetworkRequestEventId, STATIC_EXTENSIONS, xSf3RidHeader, } from "./constants";
|
|
5
5
|
import { gatherAndCacheDeviceInfo } from "./deviceInfo";
|
|
6
6
|
import { fetchCaptureSettings, sendDomainsToNotPropagateHeaderTo, startRecordingSession, } from "./graphql";
|
|
7
7
|
import { sendMapUuidIfAvailable } from "./mapUuid";
|
|
@@ -25,15 +25,106 @@ const BAD_HTTP_STATUS = [
|
|
|
25
25
|
403, // FORBIDDEN
|
|
26
26
|
];
|
|
27
27
|
const CORS_KEYWORD = "CORS";
|
|
28
|
-
const STORAGE_VERSION = 1;
|
|
29
|
-
const DYNAMIC_PASSED_HOSTS_KEY = "dynamicPassedHosts";
|
|
30
|
-
const DYNAMIC_EXCLUDED_HOSTS_KEY = "dynamicExcludedHosts";
|
|
28
|
+
export const STORAGE_VERSION = 1;
|
|
29
|
+
export const DYNAMIC_PASSED_HOSTS_KEY = "dynamicPassedHosts";
|
|
30
|
+
export const DYNAMIC_EXCLUDED_HOSTS_KEY = "dynamicExcludedHosts";
|
|
31
31
|
const SF_API_KEY_FOR_UPDATE = "sailfishApiKey";
|
|
32
32
|
const SF_BACKEND_API = "sailfishBackendApi";
|
|
33
33
|
const INCLUDE = "include";
|
|
34
34
|
const SAME_ORIGIN = "same-origin";
|
|
35
35
|
const ALLOWED_HEADERS_HEADER = "access-control-allow-headers";
|
|
36
36
|
const OPTIONS = "OPTIONS";
|
|
37
|
+
export const dynamicExcludedHosts = new Set();
|
|
38
|
+
export const dynamicPassedHosts = new Set();
|
|
39
|
+
// 1️⃣ dynamicExcludedHosts.add override
|
|
40
|
+
const originalExcludedAdd = dynamicExcludedHosts.add;
|
|
41
|
+
dynamicExcludedHosts.add = (host) => {
|
|
42
|
+
const cleaned = host?.trim();
|
|
43
|
+
if (!cleaned || dynamicExcludedHosts.has(cleaned)) {
|
|
44
|
+
return dynamicExcludedHosts;
|
|
45
|
+
}
|
|
46
|
+
// 1. Add to the excluded Set
|
|
47
|
+
originalExcludedAdd.call(dynamicExcludedHosts, cleaned);
|
|
48
|
+
// 2. If it was previously in passed, remove it
|
|
49
|
+
if (dynamicPassedHosts.has(cleaned)) {
|
|
50
|
+
dynamicPassedHosts.delete(cleaned);
|
|
51
|
+
}
|
|
52
|
+
// 3. Consolidate dynamic exclusions into smart wildcards
|
|
53
|
+
const consolidated = consolidateDynamicExclusions(dynamicExcludedHosts);
|
|
54
|
+
dynamicExcludedHosts.clear();
|
|
55
|
+
for (const p of consolidated) {
|
|
56
|
+
originalExcludedAdd.call(dynamicExcludedHosts, p);
|
|
57
|
+
}
|
|
58
|
+
// 4. Persist wrapper to localStorage
|
|
59
|
+
try {
|
|
60
|
+
// TODO decouple into two logical pieces
|
|
61
|
+
const wrapper = {
|
|
62
|
+
version: STORAGE_VERSION,
|
|
63
|
+
entries: {},
|
|
64
|
+
};
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
for (const p of dynamicExcludedHosts) {
|
|
67
|
+
wrapper.entries[p] = now;
|
|
68
|
+
}
|
|
69
|
+
localStorage.setItem(DYNAMIC_EXCLUDED_HOSTS_KEY, JSON.stringify(wrapper));
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
if (DEBUG)
|
|
73
|
+
console.warn("Persist dynamicExcludedHosts failed:", e);
|
|
74
|
+
}
|
|
75
|
+
// 5. Notify backend of the updated Set
|
|
76
|
+
updateExcludedHostsStorageAndBackend(dynamicExcludedHosts);
|
|
77
|
+
return dynamicExcludedHosts;
|
|
78
|
+
};
|
|
79
|
+
// 2️⃣ dynamicPassedHosts.add override (timestamps refreshed on every call)
|
|
80
|
+
const originalPassedAdd = dynamicPassedHosts.add;
|
|
81
|
+
dynamicPassedHosts.add = (host) => {
|
|
82
|
+
const cleaned = host?.trim();
|
|
83
|
+
if (!cleaned) {
|
|
84
|
+
return dynamicPassedHosts;
|
|
85
|
+
}
|
|
86
|
+
// 1. Persist wrapper to localStorage (update timestamp unconditionally)
|
|
87
|
+
try {
|
|
88
|
+
// TODO decouple into two logical pieces
|
|
89
|
+
const raw = localStorage.getItem(DYNAMIC_PASSED_HOSTS_KEY);
|
|
90
|
+
let wrapper;
|
|
91
|
+
if (!raw) {
|
|
92
|
+
wrapper = { version: STORAGE_VERSION, entries: {} };
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const parsed = JSON.parse(raw);
|
|
96
|
+
if (Array.isArray(parsed)) {
|
|
97
|
+
// migrate old array → wrapper
|
|
98
|
+
wrapper = {
|
|
99
|
+
version: STORAGE_VERSION,
|
|
100
|
+
entries: Object.fromEntries(parsed.map((h) => [h, Date.now()])),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
else if (parsed &&
|
|
104
|
+
typeof parsed === "object" &&
|
|
105
|
+
"entries" in parsed &&
|
|
106
|
+
typeof parsed.entries === "object") {
|
|
107
|
+
wrapper = parsed;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
wrapper = { version: STORAGE_VERSION, entries: {} };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// always set/update the timestamp
|
|
114
|
+
wrapper.entries[cleaned] = Date.now();
|
|
115
|
+
wrapper.version = STORAGE_VERSION;
|
|
116
|
+
localStorage.setItem(DYNAMIC_PASSED_HOSTS_KEY, JSON.stringify(wrapper));
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
if (DEBUG)
|
|
120
|
+
console.warn("Persist dynamicPassedHosts failed:", e);
|
|
121
|
+
}
|
|
122
|
+
// 2. Add to the passed Set if not already present
|
|
123
|
+
if (!dynamicPassedHosts.has(cleaned)) {
|
|
124
|
+
originalPassedAdd.call(dynamicPassedHosts, cleaned);
|
|
125
|
+
}
|
|
126
|
+
return dynamicPassedHosts;
|
|
127
|
+
};
|
|
37
128
|
/**
|
|
38
129
|
* Notify the backend of the updated dynamicExcludedHosts
|
|
39
130
|
*/
|
|
@@ -45,8 +136,59 @@ function updateExcludedHostsStorageAndBackend(dynamicExcludedHosts) {
|
|
|
45
136
|
}
|
|
46
137
|
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));
|
|
47
138
|
}
|
|
48
|
-
|
|
49
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Given a set of excluded hostPaths (like "foo.com/bar", "foo.com/baz", etc.),
|
|
141
|
+
* find any path prefixes under each host that appear ≥ threshold times,
|
|
142
|
+
* and replace their individual entries with a single wildcard pattern
|
|
143
|
+
* (e.g. "foo.com/bar*").
|
|
144
|
+
*/
|
|
145
|
+
export function consolidateDynamicExclusions(hostPathSet, threshold = 3) {
|
|
146
|
+
// 1️⃣ Group by host
|
|
147
|
+
const byHost = {};
|
|
148
|
+
for (const hostPath of hostPathSet) {
|
|
149
|
+
const [host, ...rest] = hostPath.split("/");
|
|
150
|
+
const path = rest.length ? `/${rest.join("/")}` : "/";
|
|
151
|
+
(byHost[host] ??= []).push(path);
|
|
152
|
+
}
|
|
153
|
+
const newSet = new Set();
|
|
154
|
+
for (const host in byHost) {
|
|
155
|
+
const paths = byHost[host];
|
|
156
|
+
if (paths.length < threshold) {
|
|
157
|
+
// not enough entries to bother; keep them as-is
|
|
158
|
+
for (const p of paths)
|
|
159
|
+
newSet.add(`${host}${p}`);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const root = { count: 0, children: new Map() };
|
|
163
|
+
for (const p of paths) {
|
|
164
|
+
root.count++;
|
|
165
|
+
const segments = p.split("/").filter(Boolean);
|
|
166
|
+
let node = root;
|
|
167
|
+
for (const seg of segments) {
|
|
168
|
+
if (!node.children.has(seg)) {
|
|
169
|
+
node.children.set(seg, { count: 0, children: new Map() });
|
|
170
|
+
}
|
|
171
|
+
node = node.children.get(seg);
|
|
172
|
+
node.count++;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// 3️⃣ Walk the trie and pick prefixes where node.count ≥ threshold
|
|
176
|
+
function gather(node, prefixSegments) {
|
|
177
|
+
// if this node covers ≥ threshold of this host’s paths, collapse here
|
|
178
|
+
if (node.count >= threshold && prefixSegments.length > 0) {
|
|
179
|
+
const wildcardPath = "/" + prefixSegments.join("/") + "/*";
|
|
180
|
+
newSet.add(`${host}${wildcardPath}`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// otherwise, recurse into children
|
|
184
|
+
for (const [seg, child] of node.children) {
|
|
185
|
+
gather(child, prefixSegments.concat(seg));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
gather(root, []);
|
|
189
|
+
}
|
|
190
|
+
return newSet;
|
|
191
|
+
}
|
|
50
192
|
// 2️⃣ Load & evict old entries (>7 days) + version check for Excluded
|
|
51
193
|
(() => {
|
|
52
194
|
const stored = localStorage.getItem(DYNAMIC_EXCLUDED_HOSTS_KEY);
|
|
@@ -102,53 +244,6 @@ const dynamicPassedHosts = new Set();
|
|
|
102
244
|
localStorage.removeItem(DYNAMIC_PASSED_HOSTS_KEY);
|
|
103
245
|
}
|
|
104
246
|
})();
|
|
105
|
-
// Override add() to persist updates to localStorage
|
|
106
|
-
const originalExcludedAdd = dynamicExcludedHosts.add;
|
|
107
|
-
dynamicExcludedHosts.add = (host) => {
|
|
108
|
-
const cleanedHost = host?.trim();
|
|
109
|
-
if (!cleanedHost || dynamicExcludedHosts.has(cleanedHost)) {
|
|
110
|
-
return dynamicExcludedHosts;
|
|
111
|
-
}
|
|
112
|
-
originalExcludedAdd.call(dynamicExcludedHosts, cleanedHost);
|
|
113
|
-
try {
|
|
114
|
-
// read existing map or start fresh
|
|
115
|
-
const stored = localStorage.getItem(DYNAMIC_EXCLUDED_HOSTS_KEY);
|
|
116
|
-
const obj = stored ? JSON.parse(stored) : {};
|
|
117
|
-
obj[cleanedHost] = Date.now();
|
|
118
|
-
localStorage.setItem(DYNAMIC_EXCLUDED_HOSTS_KEY, JSON.stringify(obj));
|
|
119
|
-
}
|
|
120
|
-
catch (e) {
|
|
121
|
-
if (DEBUG)
|
|
122
|
-
console.warn("Persist dynamicExcludedHosts failed:", e);
|
|
123
|
-
}
|
|
124
|
-
updateExcludedHostsStorageAndBackend(dynamicExcludedHosts);
|
|
125
|
-
return dynamicExcludedHosts;
|
|
126
|
-
};
|
|
127
|
-
const originalPassedAdd = dynamicPassedHosts.add;
|
|
128
|
-
// === UPDATED: override to store timestamp on add (passed hosts) ===
|
|
129
|
-
dynamicPassedHosts.add = (host) => {
|
|
130
|
-
const cleanedHost = host?.trim();
|
|
131
|
-
if (!cleanedHost) {
|
|
132
|
-
return dynamicPassedHosts;
|
|
133
|
-
}
|
|
134
|
-
// If we already have it, just return
|
|
135
|
-
if (dynamicPassedHosts.has(cleanedHost)) {
|
|
136
|
-
return dynamicPassedHosts;
|
|
137
|
-
}
|
|
138
|
-
originalPassedAdd.call(dynamicPassedHosts, cleanedHost);
|
|
139
|
-
// Persist a host->timestamp map
|
|
140
|
-
try {
|
|
141
|
-
const stored = localStorage.getItem(DYNAMIC_PASSED_HOSTS_KEY);
|
|
142
|
-
const obj = stored ? JSON.parse(stored) : {};
|
|
143
|
-
obj[cleanedHost] = Date.now();
|
|
144
|
-
localStorage.setItem(DYNAMIC_PASSED_HOSTS_KEY, JSON.stringify(obj));
|
|
145
|
-
}
|
|
146
|
-
catch (e) {
|
|
147
|
-
if (DEBUG)
|
|
148
|
-
console.warn("Persist dynamicPassedHosts failed:", e);
|
|
149
|
-
}
|
|
150
|
-
return dynamicPassedHosts;
|
|
151
|
-
};
|
|
152
247
|
const ActionType = {
|
|
153
248
|
PROPAGATE: "propagate",
|
|
154
249
|
IGNORE: "ignore",
|
|
@@ -355,75 +450,29 @@ export function matchUrlWithWildcard(url, patterns) {
|
|
|
355
450
|
return true;
|
|
356
451
|
});
|
|
357
452
|
}
|
|
358
|
-
function shouldSkipHeadersPropagation(url,
|
|
359
|
-
const
|
|
453
|
+
function shouldSkipHeadersPropagation(url, domainsToNotPropagateHeaderTo = []) {
|
|
454
|
+
const urlObj = new URL(url, window.location.href);
|
|
455
|
+
// 1️⃣ STATIC ASSET EXCLUSIONS (by comprehensive file extension list)
|
|
456
|
+
for (const ext of STATIC_EXTENSIONS) {
|
|
457
|
+
if (urlObj.pathname.toLowerCase().endsWith(ext)) {
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// 2️⃣ WILDCARD-BASED EXCLUSION (domain + path)
|
|
462
|
+
// Pass patterns like ["*.cdn.com/*", "api.example.com/v1/*"]
|
|
463
|
+
const combinedPatterns = [
|
|
360
464
|
...DOMAINS_TO_NOT_PROPAGATE_HEADER_TO_DEFAULT,
|
|
361
|
-
...
|
|
465
|
+
...domainsToNotPropagateHeaderTo,
|
|
362
466
|
];
|
|
363
|
-
|
|
364
|
-
if (defaultExcluded) {
|
|
467
|
+
if (matchUrlWithWildcard(url, combinedPatterns)) {
|
|
365
468
|
return true;
|
|
366
469
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
* Returns ActionType.PROPAGATE if server responds 2xx, ActionType.IGNORE on error or non-2xx.
|
|
374
|
-
*/
|
|
375
|
-
function performOptionsPreflightForXHR(url, init, xSf3RidHeaderValue, domain) {
|
|
376
|
-
return new Promise((resolve) => {
|
|
377
|
-
const xhr = new XMLHttpRequest();
|
|
378
|
-
xhr.open(OPTIONS, url, true);
|
|
379
|
-
xhr.withCredentials = init.credentials === INCLUDE;
|
|
380
|
-
// Normalize headers into a standard Headers instance
|
|
381
|
-
const headerEntries = new Headers(init.headers || {});
|
|
382
|
-
const rawKeys = Array.from(headerEntries.keys());
|
|
383
|
-
const customHeaders = rawKeys
|
|
384
|
-
.map((h) => h.toLowerCase())
|
|
385
|
-
.filter((h) => ![
|
|
386
|
-
"accept",
|
|
387
|
-
"content-type",
|
|
388
|
-
"accept-language",
|
|
389
|
-
"content-language",
|
|
390
|
-
].includes(h));
|
|
391
|
-
if (customHeaders.length) {
|
|
392
|
-
xhr.setRequestHeader("Access-Control-Request-Headers", customHeaders.join(","));
|
|
393
|
-
}
|
|
394
|
-
// Set the CORS preflight method header
|
|
395
|
-
xhr.setRequestHeader("Access-Control-Request-Method", (init.method || "GET").toUpperCase());
|
|
396
|
-
// Correctly add the tracing header to the XHR instance
|
|
397
|
-
xhr.setRequestHeader(xSf3RidHeader, xSf3RidHeaderValue);
|
|
398
|
-
xhr.onload = () => {
|
|
399
|
-
if (xhr.status >= 200 && xhr.status < 300) {
|
|
400
|
-
const allowed = xhr.getResponseHeader(ALLOWED_HEADERS_HEADER);
|
|
401
|
-
if (allowed &&
|
|
402
|
-
allowed
|
|
403
|
-
.split(",")
|
|
404
|
-
.map((h) => h.trim().toLowerCase())
|
|
405
|
-
.includes(xSf3RidHeader.toLowerCase())) {
|
|
406
|
-
resolve(ActionType.PROPAGATE);
|
|
407
|
-
}
|
|
408
|
-
else {
|
|
409
|
-
DEBUG &&
|
|
410
|
-
console.log(`[XHR Interceptor] Header ${xSf3RidHeader} not allowed by preflight for ${domain}`);
|
|
411
|
-
resolve(ActionType.IGNORE);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
else {
|
|
415
|
-
DEBUG &&
|
|
416
|
-
console.log(`[XHR Interceptor] OPTIONS returned status ${xhr.status} for ${domain}`);
|
|
417
|
-
resolve(null);
|
|
418
|
-
}
|
|
419
|
-
};
|
|
420
|
-
xhr.onerror = () => {
|
|
421
|
-
DEBUG &&
|
|
422
|
-
console.log(`[XHR Interceptor] Preflight OPTIONS CORS or network error for ${domain}`);
|
|
423
|
-
resolve(ActionType.IGNORE);
|
|
424
|
-
};
|
|
425
|
-
xhr.send();
|
|
426
|
-
});
|
|
470
|
+
// 3️⃣ DYNAMIC-FAILURE EXCLUSION (exact host)
|
|
471
|
+
const hostname = urlObj.hostname;
|
|
472
|
+
if (dynamicExcludedHosts.has(hostname)) {
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
return false;
|
|
427
476
|
}
|
|
428
477
|
// Updated XMLHttpRequest interceptor with domain exclusion
|
|
429
478
|
function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo = []) {
|
|
@@ -449,124 +498,90 @@ function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo = []) {
|
|
|
449
498
|
this._capturedRequestHeaders = {};
|
|
450
499
|
return originalOpen.apply(this, [method, url, ...args]);
|
|
451
500
|
};
|
|
452
|
-
//
|
|
501
|
+
// 1️⃣ XHR interceptor send()
|
|
453
502
|
XMLHttpRequest.prototype.send = function (...args) {
|
|
454
503
|
const url = this._requestUrl;
|
|
455
|
-
if (!url)
|
|
504
|
+
if (!url) {
|
|
456
505
|
return originalSend.apply(this, args);
|
|
457
|
-
|
|
458
|
-
//
|
|
506
|
+
}
|
|
507
|
+
// parse host+path for exclusion
|
|
508
|
+
const urlObj = new URL(url, window.location.href);
|
|
509
|
+
const domain = urlObj.hostname;
|
|
510
|
+
const hostPath = urlObj.pathname === "/" ? domain : `${domain}${urlObj.pathname}`;
|
|
511
|
+
// 1️⃣ Skip injection for excluded domains/paths
|
|
459
512
|
if (shouldSkipHeadersPropagation(url, domainsToNotPropagateHeaderTo)) {
|
|
460
513
|
return originalSend.apply(this, args);
|
|
461
514
|
}
|
|
515
|
+
// 2️⃣ Prepare header and IDs
|
|
462
516
|
const pageVisitUUID = sessionStorage.getItem("pageVisitUUID");
|
|
463
517
|
const networkUUID = uuidv4();
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
518
|
+
const headerValue = `${sessionId}/${pageVisitUUID}/${networkUUID}`;
|
|
519
|
+
try {
|
|
520
|
+
this.setRequestHeader(xSf3RidHeader, headerValue);
|
|
521
|
+
}
|
|
522
|
+
catch (e) {
|
|
523
|
+
console.warn(`Could not set X-Sf3-Rid for ${url}`, e);
|
|
524
|
+
}
|
|
525
|
+
// 3️⃣ Track timing
|
|
526
|
+
const startTime = Date.now();
|
|
527
|
+
let finished = false;
|
|
528
|
+
// 4️⃣ Helper to emit networkRequestFinished
|
|
529
|
+
const emitFinished = (success, status, errorMsg) => {
|
|
530
|
+
if (finished)
|
|
531
|
+
return;
|
|
532
|
+
finished = true;
|
|
533
|
+
const endTime = Date.now();
|
|
534
|
+
sendEvent({
|
|
535
|
+
type: NetworkRequestEventId,
|
|
536
|
+
timestamp: endTime,
|
|
537
|
+
sessionId,
|
|
538
|
+
data: {
|
|
539
|
+
request_id: networkUUID,
|
|
540
|
+
session_id: sessionId,
|
|
541
|
+
timestamp_start: startTime,
|
|
542
|
+
timestamp_end: endTime,
|
|
543
|
+
response_code: status,
|
|
544
|
+
success,
|
|
545
|
+
error: errorMsg,
|
|
546
|
+
method: this._requestMethod,
|
|
547
|
+
url,
|
|
548
|
+
},
|
|
549
|
+
...getUrlAndStoredUuids(),
|
|
550
|
+
});
|
|
551
|
+
// 5️⃣ Update dynamic sets
|
|
552
|
+
if (success) {
|
|
553
|
+
// once any route passes, skip preflight on entire domain
|
|
554
|
+
dynamicPassedHosts.add(domain);
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
// only exclude the specific failing path
|
|
558
|
+
dynamicExcludedHosts.add(hostPath);
|
|
473
559
|
}
|
|
474
|
-
// On CORS or network error during send, log it and add to excluded-hosts
|
|
475
|
-
this.addEventListener("error", (evt) => {
|
|
476
|
-
console.error(`[XHR Interceptor] Network error for ${domain} (${url}):`, evt, `status=${this.status}`, `statusText=${this.statusText}`);
|
|
477
|
-
dynamicExcludedHosts.add(domain);
|
|
478
|
-
}, { once: true });
|
|
479
|
-
// On load, log non-2xx statuses (including CORS-blocked status=0) and track passes
|
|
480
|
-
this.addEventListener("load", () => {
|
|
481
|
-
if (this.status === 0) {
|
|
482
|
-
DEBUG &&
|
|
483
|
-
console.error(`[XHR Interceptor] CORS blocked (status=0) for ${domain} (${url})`, `statusText=${this.statusText}`);
|
|
484
|
-
dynamicExcludedHosts.add(domain);
|
|
485
|
-
}
|
|
486
|
-
else if (this.status >= 200 && this.status < 300) {
|
|
487
|
-
dynamicPassedHosts.add(domain);
|
|
488
|
-
}
|
|
489
|
-
else {
|
|
490
|
-
DEBUG &&
|
|
491
|
-
console.error(`[XHR Interceptor] HTTP error ${this.status} ${this.statusText} for ${domain} (${url})`);
|
|
492
|
-
dynamicExcludedHosts.add(domain);
|
|
493
|
-
}
|
|
494
|
-
}, { once: true });
|
|
495
|
-
return originalSend.apply(this, args);
|
|
496
560
|
};
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
.
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
// just return void
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
return proceedSend();
|
|
561
|
+
// 6️⃣ On successful load
|
|
562
|
+
this.addEventListener("load", () => {
|
|
563
|
+
const status = this.status || 0;
|
|
564
|
+
if (status >= 200 && status < 300) {
|
|
565
|
+
emitFinished(true, status, "");
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
const msg = this.statusText || `HTTP ${status}`;
|
|
569
|
+
emitFinished(false, status, msg);
|
|
570
|
+
}
|
|
571
|
+
}, { once: true });
|
|
572
|
+
// 7️⃣ On network/CORS error
|
|
573
|
+
this.addEventListener("error", () => {
|
|
574
|
+
// XHR error events often mean CORS or connectivity; status is usually 0
|
|
575
|
+
const status = this.status || 0;
|
|
576
|
+
const msg = status === 0
|
|
577
|
+
? "Network or CORS failure"
|
|
578
|
+
: this.statusText || `Error ${status}`;
|
|
579
|
+
emitFinished(false, status, msg);
|
|
580
|
+
}, { once: true });
|
|
581
|
+
// 8️⃣ Finally, send the request
|
|
582
|
+
return originalSend.apply(this, args);
|
|
523
583
|
};
|
|
524
584
|
}
|
|
525
|
-
/**
|
|
526
|
-
* Performs an OPTIONS preflight check to decide header propagation.
|
|
527
|
-
* Returns PROPAGATE if both header & method are allowed, IGNORE if disallowed
|
|
528
|
-
* or on a CORS error, null if OPTIONS returned non-2xx.
|
|
529
|
-
*/
|
|
530
|
-
async function performOptionsPreflight(target, thisArg, url, init, sessionId, domain) {
|
|
531
|
-
try {
|
|
532
|
-
const headers = new Headers(init.headers || {});
|
|
533
|
-
headers.set(xSf3RidHeader, sessionId);
|
|
534
|
-
const opts = {
|
|
535
|
-
method: OPTIONS,
|
|
536
|
-
headers,
|
|
537
|
-
mode: "cors",
|
|
538
|
-
credentials: init.credentials || SAME_ORIGIN,
|
|
539
|
-
};
|
|
540
|
-
const response = await target.call(thisArg, url, opts);
|
|
541
|
-
if (!response.ok) {
|
|
542
|
-
DEBUG &&
|
|
543
|
-
console.log(`[Fetch Interceptor] OPTIONS returned status ${response.status} for ${domain}`);
|
|
544
|
-
return null;
|
|
545
|
-
}
|
|
546
|
-
// 1️⃣ Check that our header is allowed
|
|
547
|
-
const allowedHeaders = response.headers
|
|
548
|
-
.get(ALLOWED_HEADERS_HEADER)
|
|
549
|
-
?.split(",")
|
|
550
|
-
.map((h) => h.trim().toLowerCase()) || [];
|
|
551
|
-
if (!allowedHeaders.includes(xSf3RidHeader.toLowerCase())) {
|
|
552
|
-
DEBUG &&
|
|
553
|
-
console.log(`[Fetch Interceptor] Header ${xSf3RidHeader} not allowed by preflight for ${domain}`);
|
|
554
|
-
return ActionType.IGNORE;
|
|
555
|
-
}
|
|
556
|
-
return ActionType.PROPAGATE;
|
|
557
|
-
}
|
|
558
|
-
catch (error) {
|
|
559
|
-
if (error instanceof TypeError &&
|
|
560
|
-
error.message.toLowerCase().includes(CORS_KEYWORD.toLowerCase())) {
|
|
561
|
-
DEBUG &&
|
|
562
|
-
console.log(`[Fetch Interceptor] Preflight OPTIONS CORS error for ${domain}:`, error);
|
|
563
|
-
return ActionType.IGNORE;
|
|
564
|
-
}
|
|
565
|
-
DEBUG &&
|
|
566
|
-
console.log(`[Fetch Interceptor] Preflight OPTIONS failed for ${domain}:`, error);
|
|
567
|
-
return null;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
585
|
// Updated fetch interceptor with exclusion handling
|
|
571
586
|
function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
572
587
|
const originalFetch = window.fetch;
|
|
@@ -577,6 +592,7 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
577
592
|
let input = args[0];
|
|
578
593
|
let init = args[1] || {};
|
|
579
594
|
let url;
|
|
595
|
+
// 1️⃣ Normalize URL string
|
|
580
596
|
if (typeof input === "string") {
|
|
581
597
|
url = input;
|
|
582
598
|
}
|
|
@@ -587,130 +603,103 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
587
603
|
url = input.href;
|
|
588
604
|
}
|
|
589
605
|
else {
|
|
590
|
-
return target.apply(thisArg, args);
|
|
606
|
+
return target.apply(thisArg, args);
|
|
591
607
|
}
|
|
592
|
-
// Determine the target domain
|
|
593
608
|
const domain = new URL(url, window.location.href).hostname;
|
|
594
|
-
//
|
|
595
|
-
if (cachedDomains.has(domain)) {
|
|
596
|
-
const decision = cachedDomains.get(domain);
|
|
597
|
-
if (decision === ActionType.IGNORE) {
|
|
598
|
-
return target.apply(thisArg, args);
|
|
599
|
-
}
|
|
600
|
-
if (decision === ActionType.PROPAGATE) {
|
|
601
|
-
return injectHeaderWrapper(target, thisArg, args, input, init, sessionId, url);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
// Check exclusion domains and cache 'ignore'
|
|
609
|
+
// 2️⃣ Skip header injection if excluded
|
|
605
610
|
if (shouldSkipHeadersPropagation(url, domainsToNotPropagateHeadersTo)) {
|
|
606
|
-
cachedDomains.set(domain, ActionType.IGNORE);
|
|
607
|
-
return target.apply(thisArg, args);
|
|
608
|
-
}
|
|
609
|
-
let decision = ActionType.PROPAGATE;
|
|
610
|
-
// Check if domain verified before
|
|
611
|
-
if (!dynamicPassedHosts.has(domain)) {
|
|
612
|
-
// Perform OPTIONS preflight to decide header propagation
|
|
613
|
-
const res = await performOptionsPreflight(target, thisArg, url, init, sessionId, domain);
|
|
614
|
-
// Skip the header propagation as OPTIONS return Ignore
|
|
615
|
-
if (res === ActionType.IGNORE) {
|
|
616
|
-
decision = res;
|
|
617
|
-
dynamicExcludedHosts.add(domain);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
cachedDomains.set(domain, decision);
|
|
621
|
-
if (decision === ActionType.PROPAGATE) {
|
|
622
|
-
return injectHeaderWrapper(target, thisArg, args, input, init, sessionId, url);
|
|
623
|
-
}
|
|
624
|
-
else {
|
|
625
611
|
return target.apply(thisArg, args);
|
|
626
612
|
}
|
|
613
|
+
// 3️⃣ Delegate to our existing wrapper that injects header,
|
|
614
|
+
// handles retries on BAD_HTTP_STATUS, and updates dynamic sets.
|
|
615
|
+
return injectHeaderWrapper(target, thisArg, args, input, init, sessionId, url);
|
|
627
616
|
},
|
|
628
617
|
});
|
|
629
|
-
//
|
|
618
|
+
// 2️⃣ Fetch interceptor’s injectHeaderWrapper(); emits 'networkRequest' event
|
|
630
619
|
async function injectHeaderWrapper(target, thisArg, args, input, init, sessionId, url) {
|
|
631
|
-
if (sessionId) {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
const success = response.ok;
|
|
650
|
-
const error = success ? "" : `Request Error: ${response.statusText}`;
|
|
651
|
-
if (success) {
|
|
652
|
-
(isRetry ? dynamicExcludedHosts : dynamicPassedHosts).add(domain);
|
|
653
|
-
}
|
|
654
|
-
// Emit 'networkRequestFinished' event
|
|
655
|
-
const eventData = {
|
|
656
|
-
type: NetworkRequestEventId,
|
|
657
|
-
timestamp: endTime,
|
|
658
|
-
sessionId,
|
|
659
|
-
data: {
|
|
660
|
-
request_id: networkUUID,
|
|
661
|
-
session_id: sessionId,
|
|
662
|
-
timestamp_start: startTime,
|
|
663
|
-
timestamp_end: endTime,
|
|
664
|
-
response_code: status,
|
|
665
|
-
success,
|
|
666
|
-
error,
|
|
667
|
-
method,
|
|
668
|
-
url,
|
|
669
|
-
},
|
|
670
|
-
...urlAndStoredUuids,
|
|
671
|
-
};
|
|
672
|
-
sendEvent(eventData);
|
|
673
|
-
return response;
|
|
620
|
+
if (!sessionId) {
|
|
621
|
+
return target.apply(thisArg, args);
|
|
622
|
+
}
|
|
623
|
+
const urlObj = new URL(url, window.location.href);
|
|
624
|
+
const domain = urlObj.hostname;
|
|
625
|
+
const hostPath = urlObj.pathname === "/" ? domain : `${domain}${urlObj.pathname}`;
|
|
626
|
+
const networkUUID = uuidv4();
|
|
627
|
+
const urlAndStoredUuids = getUrlAndStoredUuids();
|
|
628
|
+
const method = init.method || "GET";
|
|
629
|
+
const startTime = Date.now();
|
|
630
|
+
try {
|
|
631
|
+
let response = await injectHeader(target, thisArg, input, init, sessionId, urlAndStoredUuids.page_visit_uuid, networkUUID);
|
|
632
|
+
let isRetry = false;
|
|
633
|
+
// Retry logic for 400/403 before logging finished event
|
|
634
|
+
if (BAD_HTTP_STATUS.includes(response.status)) {
|
|
635
|
+
DEBUG && console.log("Perform retry as status was fail:", response);
|
|
636
|
+
response = await retryWithoutPropagateHeaders(target, thisArg, args, url);
|
|
637
|
+
isRetry = true;
|
|
674
638
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
// 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.
|
|
682
|
-
if (error instanceof TypeError &&
|
|
683
|
-
error?.message?.toLowerCase().includes(CORS_KEYWORD.toLowerCase())) {
|
|
684
|
-
DEBUG &&
|
|
685
|
-
console.log(`[Fetch Interceptor] CORS error for ${domain}:`, error);
|
|
686
|
-
dynamicExcludedHosts.add(domain);
|
|
687
|
-
return target.apply(thisArg, args);
|
|
688
|
-
}
|
|
639
|
+
const endTime = Date.now();
|
|
640
|
+
const status = response.status;
|
|
641
|
+
const success = response.ok;
|
|
642
|
+
const error = success ? "" : `Request Error: ${response.statusText}`;
|
|
643
|
+
// 5️⃣ Update dynamic sets
|
|
644
|
+
if (success) {
|
|
689
645
|
dynamicPassedHosts.add(domain);
|
|
690
|
-
// Emit 'networkRequestFinished' event with error
|
|
691
|
-
const eventData = {
|
|
692
|
-
type: NetworkRequestEventId,
|
|
693
|
-
timestamp: endTime,
|
|
694
|
-
sessionId,
|
|
695
|
-
data: {
|
|
696
|
-
request_id: networkUUID,
|
|
697
|
-
session_id: sessionId,
|
|
698
|
-
timestamp_start: startTime,
|
|
699
|
-
timestamp_end: endTime,
|
|
700
|
-
response_code: responseCode,
|
|
701
|
-
success,
|
|
702
|
-
error: errorMessage,
|
|
703
|
-
method,
|
|
704
|
-
url,
|
|
705
|
-
},
|
|
706
|
-
...urlAndStoredUuids,
|
|
707
|
-
};
|
|
708
|
-
sendEvent(eventData);
|
|
709
|
-
throw error;
|
|
710
646
|
}
|
|
647
|
+
else {
|
|
648
|
+
dynamicExcludedHosts.add(hostPath);
|
|
649
|
+
}
|
|
650
|
+
// Emit 'networkRequestFinished' event
|
|
651
|
+
const eventData = {
|
|
652
|
+
type: NetworkRequestEventId,
|
|
653
|
+
timestamp: endTime,
|
|
654
|
+
sessionId,
|
|
655
|
+
data: {
|
|
656
|
+
request_id: networkUUID,
|
|
657
|
+
session_id: sessionId,
|
|
658
|
+
timestamp_start: startTime,
|
|
659
|
+
timestamp_end: endTime,
|
|
660
|
+
response_code: status,
|
|
661
|
+
success,
|
|
662
|
+
error,
|
|
663
|
+
method,
|
|
664
|
+
url,
|
|
665
|
+
},
|
|
666
|
+
...urlAndStoredUuids,
|
|
667
|
+
};
|
|
668
|
+
sendEvent(eventData);
|
|
669
|
+
return response;
|
|
711
670
|
}
|
|
712
|
-
|
|
713
|
-
|
|
671
|
+
catch (error) {
|
|
672
|
+
const endTime = Date.now();
|
|
673
|
+
const success = false;
|
|
674
|
+
const responseCode = error.response?.status || 500;
|
|
675
|
+
const errorMessage = error.message || "Fetch request failed";
|
|
676
|
+
if (error instanceof TypeError &&
|
|
677
|
+
error?.message?.toLowerCase().includes(CORS_KEYWORD.toLowerCase())) {
|
|
678
|
+
// CORS/network failure: exclude just this path
|
|
679
|
+
dynamicExcludedHosts.add(hostPath);
|
|
680
|
+
return target.apply(thisArg, args);
|
|
681
|
+
}
|
|
682
|
+
// On other errors, treat as “passed” so we don’t re-preflight
|
|
683
|
+
dynamicPassedHosts.add(domain);
|
|
684
|
+
const eventData = {
|
|
685
|
+
type: NetworkRequestEventId,
|
|
686
|
+
timestamp: endTime,
|
|
687
|
+
sessionId,
|
|
688
|
+
data: {
|
|
689
|
+
request_id: networkUUID,
|
|
690
|
+
session_id: sessionId,
|
|
691
|
+
timestamp_start: startTime,
|
|
692
|
+
timestamp_end: endTime,
|
|
693
|
+
response_code: responseCode,
|
|
694
|
+
success,
|
|
695
|
+
error: errorMessage,
|
|
696
|
+
method,
|
|
697
|
+
url,
|
|
698
|
+
},
|
|
699
|
+
...urlAndStoredUuids,
|
|
700
|
+
};
|
|
701
|
+
sendEvent(eventData);
|
|
702
|
+
throw error;
|
|
714
703
|
}
|
|
715
704
|
}
|
|
716
705
|
// Helper function to inject the X-Sf3-Rid header
|
|
@@ -789,28 +778,24 @@ export async function startRecording({ apiKey, backendApi = "https://api-service
|
|
|
789
778
|
initializeConsolePlugin(DEFAULT_CONSOLE_RECORDING_SETTINGS, sessionId);
|
|
790
779
|
storeCredentialsAndConnection({ apiKey, backendApi });
|
|
791
780
|
trackDomainChanges();
|
|
792
|
-
//
|
|
793
|
-
|
|
794
|
-
|
|
781
|
+
// ─── Merge stored excludes + passed-in excludes ───
|
|
782
|
+
const initialExcludes = new Set(dynamicExcludedHosts);
|
|
783
|
+
domainsToNotPropagateHeaderTo.forEach((host) => {
|
|
784
|
+
if (host?.trim()) {
|
|
785
|
+
dynamicExcludedHosts.add(host.trim());
|
|
786
|
+
}
|
|
795
787
|
});
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
788
|
+
const newlyExcluded = Array.from(dynamicExcludedHosts).filter((h) => !initialExcludes.has(h));
|
|
789
|
+
if (newlyExcluded.length) {
|
|
790
|
+
// single notify of the full updated list
|
|
791
|
+
updateExcludedHostsStorageAndBackend(dynamicExcludedHosts);
|
|
799
792
|
}
|
|
800
|
-
|
|
801
|
-
// Add provided domainsToPropagateHeaderTo to dynamicPassedHosts
|
|
793
|
+
// ─── Merge passed hosts ───
|
|
802
794
|
domainsToPropagateHeaderTo.forEach((host) => {
|
|
803
|
-
|
|
795
|
+
if (host?.trim()) {
|
|
796
|
+
dynamicPassedHosts.add(host.trim());
|
|
797
|
+
}
|
|
804
798
|
});
|
|
805
|
-
// Persist updated included hosts to localStorage
|
|
806
|
-
try {
|
|
807
|
-
localStorage.setItem(DYNAMIC_PASSED_HOSTS_KEY, JSON.stringify(Array.from(dynamicPassedHosts)));
|
|
808
|
-
}
|
|
809
|
-
catch { }
|
|
810
|
-
// Non-blocking GraphQL request to send the domains if provided
|
|
811
|
-
if (dynamicExcludedHosts.size > 0) {
|
|
812
|
-
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));
|
|
813
|
-
}
|
|
814
799
|
sessionStorage.setItem(SF_API_KEY_FOR_UPDATE, apiKey);
|
|
815
800
|
sessionStorage.setItem(SF_BACKEND_API, backendApi);
|
|
816
801
|
// Setup interceptors with custom ignore and propagate domains
|