@sailfish-ai/recorder 1.8.2 → 1.8.8
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/README.md +4 -0
- package/dist/errorInterceptor.js +74 -56
- package/dist/inAppReportIssueModal.js +18 -2
- package/dist/index.js +209 -12
- package/dist/recorder.cjs +954 -804
- package/dist/recorder.js +1040 -886
- package/dist/recorder.js.br +0 -0
- package/dist/recorder.js.gz +0 -0
- package/dist/recorder.umd.cjs +955 -805
- package/dist/recording.js +2 -0
- package/dist/types/errorInterceptor.d.ts +0 -5
- package/dist/types/recording.d.ts +2 -0
- package/dist/types/websocket.d.ts +23 -0
- package/dist/websocket.js +244 -7
- package/package.json +2 -1
package/README.md
CHANGED
package/dist/errorInterceptor.js
CHANGED
|
@@ -1,73 +1,89 @@
|
|
|
1
1
|
import { SourceMapConsumer } from "source-map-js";
|
|
2
2
|
import { sendMessage } from "./websocket";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
const mapCache = new Map();
|
|
4
|
+
// Matches: url-or-path/to/file.js?cache=123:LINE:COL (also if wrapped in "foo (...)" )
|
|
5
|
+
const FLEX_RE = /(?:\(|\s|^)(https?:\/\/[^)\s]+|\/[^)\s]+|[^)\s]+)?\/?([^/]+\.js)(?:\?[^:)]*)?:(\d+):(\d+)/;
|
|
6
|
+
async function getConsumerFor(fullPathOrUrl, fileBase) {
|
|
7
|
+
// Build a list of candidate .map URLs (strip query, try same-dir first, then /assets)
|
|
8
|
+
const baseNoQuery = (fullPathOrUrl || `/assets/${fileBase}`).split("?")[0];
|
|
9
|
+
const candidates = [
|
|
10
|
+
`${baseNoQuery}.map`,
|
|
11
|
+
// Vite deps fallback:
|
|
12
|
+
baseNoQuery.replace(/\.js$/, ".js.map"),
|
|
13
|
+
// Legacy assets fallback:
|
|
14
|
+
`/assets/${fileBase}.map`,
|
|
15
|
+
];
|
|
16
|
+
for (const url of candidates) {
|
|
17
|
+
try {
|
|
18
|
+
if (mapCache.has(url))
|
|
19
|
+
return mapCache.get(url);
|
|
20
|
+
const res = await fetch(url);
|
|
21
|
+
if (!res.ok)
|
|
22
|
+
continue;
|
|
23
|
+
const raw = (await res.json());
|
|
24
|
+
if (!raw || !raw.mappings || !raw.sources)
|
|
25
|
+
continue;
|
|
26
|
+
const consumer = await new SourceMapConsumer(raw);
|
|
27
|
+
mapCache.set(url, consumer);
|
|
28
|
+
return consumer;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// try next candidate
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
8
36
|
export async function resolveStackTrace(stackTrace) {
|
|
9
37
|
if (!stackTrace)
|
|
10
38
|
return ["No stack trace available"];
|
|
11
39
|
const traceLines = Array.isArray(stackTrace)
|
|
12
40
|
? stackTrace
|
|
13
41
|
: stackTrace.split("\n");
|
|
14
|
-
const
|
|
15
|
-
for (const
|
|
16
|
-
const
|
|
17
|
-
if (!
|
|
18
|
-
|
|
42
|
+
const out = [];
|
|
43
|
+
for (const rawLine of traceLines) {
|
|
44
|
+
const m = rawLine.match(FLEX_RE);
|
|
45
|
+
if (!m) {
|
|
46
|
+
out.push(rawLine);
|
|
19
47
|
continue;
|
|
20
48
|
}
|
|
21
|
-
const [,
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// Define range (clamp within file bounds)
|
|
49
|
-
const start = Math.max(errorLine - 6, 0); // -6 because index is 0-based
|
|
50
|
-
const end = Math.min(errorLine + 4, lines.length); // +4 to get 5 lines after
|
|
51
|
-
// Extract and trim context lines
|
|
52
|
-
contextSnippet = lines.slice(start, end).map((line, i) => {
|
|
53
|
-
const lineNumber = start + i + 1;
|
|
54
|
-
const prefix = lineNumber === errorLine ? "👉" : " ";
|
|
55
|
-
return `${prefix} ${lineNumber
|
|
56
|
-
.toString()
|
|
57
|
-
.padStart(4)} | ${line.trim()}`;
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
mappedStack.push(`${original.source}:${original.line}:${original.column} (${original.name || "anonymous"})`);
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
mappedStack.push(line);
|
|
49
|
+
const [, fullPathOrUrl, fileBase, lineStr, colStr] = m;
|
|
50
|
+
const genLine = parseInt(lineStr, 10); // 1-based ✅
|
|
51
|
+
const genCol = Math.max(0, parseInt(colStr, 10) - 1); // 0-based ✅
|
|
52
|
+
if (!Number.isFinite(genLine) || !Number.isFinite(genCol)) {
|
|
53
|
+
out.push(rawLine + " [Invalid line/column]");
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const consumer = await getConsumerFor(fullPathOrUrl, fileBase);
|
|
57
|
+
if (!consumer) {
|
|
58
|
+
out.push(`${rawLine} [No source map found for ${fileBase}]`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
// Try exact col, then small backoff
|
|
62
|
+
let mapped = consumer.originalPositionFor({
|
|
63
|
+
line: genLine,
|
|
64
|
+
column: genCol,
|
|
65
|
+
bias: SourceMapConsumer.GREATEST_LOWER_BOUND,
|
|
66
|
+
});
|
|
67
|
+
if (!mapped.source || mapped.line == null) {
|
|
68
|
+
for (let d = 1; d <= 20; d++) {
|
|
69
|
+
mapped = consumer.originalPositionFor({
|
|
70
|
+
line: genLine,
|
|
71
|
+
column: Math.max(0, genCol - d),
|
|
72
|
+
bias: SourceMapConsumer.GREATEST_LOWER_BOUND,
|
|
73
|
+
});
|
|
74
|
+
if (mapped.source && mapped.line != null)
|
|
75
|
+
break;
|
|
64
76
|
}
|
|
65
77
|
}
|
|
66
|
-
|
|
67
|
-
|
|
78
|
+
if (mapped.source && mapped.line != null) {
|
|
79
|
+
const fn = mapped.name || "anonymous";
|
|
80
|
+
out.push(`${mapped.source}:${mapped.line}:${mapped.column ?? 0} (${fn})`);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
out.push(`${rawLine} [No mapping found in ${fileBase}]`);
|
|
68
84
|
}
|
|
69
85
|
}
|
|
70
|
-
return
|
|
86
|
+
return out;
|
|
71
87
|
}
|
|
72
88
|
/**
|
|
73
89
|
* Captures full error details and resolves the stack trace.
|
|
@@ -89,9 +105,11 @@ async function captureError(error, isPromiseRejection = false) {
|
|
|
89
105
|
}
|
|
90
106
|
const mappedStack = await resolveStackTrace(stack);
|
|
91
107
|
const filteredStack = mappedStack.filter((line) => !line.includes("chunk-") && !line.includes("react-dom"));
|
|
108
|
+
const trace = filteredStack.length > 0 ? filteredStack : mappedStack;
|
|
92
109
|
const errorDetails = {
|
|
93
110
|
message: errorMessage,
|
|
94
111
|
stack,
|
|
112
|
+
trace,
|
|
95
113
|
filteredStack,
|
|
96
114
|
userAgent: navigator.userAgent,
|
|
97
115
|
url: window.location.href,
|
|
@@ -674,7 +674,7 @@ function startCountdownThenRecord() {
|
|
|
674
674
|
let count = 3;
|
|
675
675
|
overlay.textContent = count.toString();
|
|
676
676
|
document.body.appendChild(overlay);
|
|
677
|
-
const interval = setInterval(() => {
|
|
677
|
+
const interval = setInterval(async () => {
|
|
678
678
|
count--;
|
|
679
679
|
if (count > 0) {
|
|
680
680
|
overlay.textContent = count.toString();
|
|
@@ -685,6 +685,14 @@ function startCountdownThenRecord() {
|
|
|
685
685
|
// Begin recording
|
|
686
686
|
recordingStartTime = Date.now();
|
|
687
687
|
isRecording = true;
|
|
688
|
+
// Enable function span tracking for this recording session
|
|
689
|
+
try {
|
|
690
|
+
const { enableFunctionSpanTracking } = await import("./websocket");
|
|
691
|
+
enableFunctionSpanTracking();
|
|
692
|
+
}
|
|
693
|
+
catch (e) {
|
|
694
|
+
console.error("[Report Issue] Failed to enable function span tracking:", e);
|
|
695
|
+
}
|
|
688
696
|
closeModal();
|
|
689
697
|
showFloatingTimer();
|
|
690
698
|
}
|
|
@@ -742,12 +750,20 @@ function showFloatingTimer() {
|
|
|
742
750
|
timerEl.textContent = `${mins}:${secs}`;
|
|
743
751
|
}, 1000);
|
|
744
752
|
}
|
|
745
|
-
function stopRecording() {
|
|
753
|
+
async function stopRecording() {
|
|
746
754
|
recordingEndTime = Date.now();
|
|
747
755
|
isRecording = false;
|
|
748
756
|
if (timerInterval)
|
|
749
757
|
clearInterval(timerInterval);
|
|
750
758
|
document.getElementById("sf-recording-indicator")?.remove();
|
|
759
|
+
// Disable function span tracking after recording stops
|
|
760
|
+
try {
|
|
761
|
+
const { disableFunctionSpanTracking } = await import("./websocket");
|
|
762
|
+
disableFunctionSpanTracking();
|
|
763
|
+
}
|
|
764
|
+
catch (e) {
|
|
765
|
+
console.error("[Report Issue] Failed to disable function span tracking:", e);
|
|
766
|
+
}
|
|
751
767
|
reopenModalAfterStop();
|
|
752
768
|
}
|
|
753
769
|
function reopenModalAfterStop() {
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { getUrlAndStoredUuids, initializeConsolePlugin, initializeDomContentEven
|
|
|
12
12
|
import { HAS_DOCUMENT, HAS_LOCAL_STORAGE, HAS_SESSION_STORAGE, HAS_WINDOW, } from "./runtimeEnv";
|
|
13
13
|
import { getOrSetSessionId } from "./session";
|
|
14
14
|
import { withAppUrlMetadata } from "./utils";
|
|
15
|
-
import { sendEvent, sendMessage } from "./websocket";
|
|
15
|
+
import { getFuncSpanHeader, sendEvent, sendMessage } from "./websocket";
|
|
16
16
|
const DEBUG = readDebugFlag(); // A wrapper around fetch that suppresses connection refused errors
|
|
17
17
|
// Default list of domains to ignore
|
|
18
18
|
const DOMAINS_TO_NOT_PROPAGATE_HEADER_TO_DEFAULT = [
|
|
@@ -167,15 +167,42 @@ function getOrSetUserDeviceUuid() {
|
|
|
167
167
|
}
|
|
168
168
|
// Function to handle resetting the sessionId when the page becomes visible again
|
|
169
169
|
function handleVisibilityChange() {
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
const visibilityState = document.visibilityState;
|
|
171
|
+
const timestamp = Date.now();
|
|
172
|
+
// Restore sessionId when the user returns to the page
|
|
173
|
+
if (visibilityState === "visible") {
|
|
174
|
+
getOrSetSessionId();
|
|
172
175
|
}
|
|
176
|
+
// Send visibility change event for both visible and hidden states
|
|
177
|
+
try {
|
|
178
|
+
const url = window.location.href.split("?")[0];
|
|
179
|
+
sendMessage({
|
|
180
|
+
type: "visibilityChange",
|
|
181
|
+
data: {
|
|
182
|
+
state: visibilityState,
|
|
183
|
+
url,
|
|
184
|
+
timestamp,
|
|
185
|
+
...getUrlAndStoredUuids(),
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
if (DEBUG) {
|
|
189
|
+
console.log(`[Sailfish] Tab became ${visibilityState}, sent visibility change event`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
console.warn("[Sailfish] Failed to send visibility change event:", error);
|
|
194
|
+
}
|
|
195
|
+
// Store visibility state in sessionStorage
|
|
196
|
+
sessionStorage.setItem("tabVisibilityChanged", timestamp.toString());
|
|
197
|
+
sessionStorage.setItem("tabVisibilityState", visibilityState);
|
|
173
198
|
}
|
|
174
|
-
function
|
|
199
|
+
function clearPageVisitDataFromSessionStorage() {
|
|
175
200
|
if (!HAS_SESSION_STORAGE)
|
|
176
201
|
return;
|
|
177
202
|
sessionStorage.removeItem("pageVisitUUID");
|
|
178
203
|
sessionStorage.removeItem("prevPageVisitUUID");
|
|
204
|
+
sessionStorage.removeItem("tabVisibilityChanged");
|
|
205
|
+
sessionStorage.removeItem("tabVisibilityState");
|
|
179
206
|
}
|
|
180
207
|
// Initialize event listeners for visibility change and page unload
|
|
181
208
|
if (HAS_DOCUMENT) {
|
|
@@ -183,8 +210,8 @@ if (HAS_DOCUMENT) {
|
|
|
183
210
|
}
|
|
184
211
|
if (HAS_WINDOW) {
|
|
185
212
|
window.addEventListener("beforeunload", () => {
|
|
186
|
-
window.name = "";
|
|
187
|
-
|
|
213
|
+
// window.name = "";
|
|
214
|
+
clearPageVisitDataFromSessionStorage();
|
|
188
215
|
});
|
|
189
216
|
}
|
|
190
217
|
function storeCredentialsAndConnection({ apiKey, backendApi, }) {
|
|
@@ -353,11 +380,36 @@ function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo = []) {
|
|
|
353
380
|
catch (e) {
|
|
354
381
|
console.warn(`Could not set X-Sf3-Rid for ${url}`, e);
|
|
355
382
|
}
|
|
356
|
-
//
|
|
383
|
+
// Add funcspan header if tracking is enabled
|
|
384
|
+
const funcSpanHeader = getFuncSpanHeader();
|
|
385
|
+
if (funcSpanHeader) {
|
|
386
|
+
try {
|
|
387
|
+
this.setRequestHeader(funcSpanHeader.name, funcSpanHeader.value);
|
|
388
|
+
if (DEBUG) {
|
|
389
|
+
console.log(`[Sailfish] Added funcspan header to XMLHttpRequest:`, {
|
|
390
|
+
url,
|
|
391
|
+
header: funcSpanHeader.name,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
catch (e) {
|
|
396
|
+
if (DEBUG) {
|
|
397
|
+
console.warn(`[Sailfish] Could not set funcspan header for ${url}`, e);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// 3️⃣ Track timing and capture request data
|
|
357
402
|
const startTime = Date.now();
|
|
358
403
|
let finished = false;
|
|
404
|
+
const requestBody = args[0]; // Capture the request body/payload
|
|
405
|
+
const requestHeaders = { ...this._capturedRequestHeaders }; // Capture request headers
|
|
406
|
+
// Filter out internal Sailfish headers from the captured headers
|
|
407
|
+
delete requestHeaders[xSf3RidHeader];
|
|
408
|
+
if (funcSpanHeader) {
|
|
409
|
+
delete requestHeaders[funcSpanHeader.name];
|
|
410
|
+
}
|
|
359
411
|
// 4️⃣ Helper to emit networkRequestFinished
|
|
360
|
-
const emitFinished = (success, status, errorMsg) => {
|
|
412
|
+
const emitFinished = (success, status, errorMsg, responseData, responseHeaders) => {
|
|
361
413
|
if (finished)
|
|
362
414
|
return;
|
|
363
415
|
finished = true;
|
|
@@ -376,6 +428,10 @@ function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo = []) {
|
|
|
376
428
|
error: errorMsg,
|
|
377
429
|
method: this._requestMethod,
|
|
378
430
|
url,
|
|
431
|
+
request_headers: requestHeaders,
|
|
432
|
+
request_body: requestBody,
|
|
433
|
+
response_headers: responseHeaders,
|
|
434
|
+
response_body: responseData,
|
|
379
435
|
},
|
|
380
436
|
...getUrlAndStoredUuids(),
|
|
381
437
|
});
|
|
@@ -383,12 +439,42 @@ function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo = []) {
|
|
|
383
439
|
// 6️⃣ On successful load
|
|
384
440
|
this.addEventListener("load", () => {
|
|
385
441
|
const status = this.status || 0;
|
|
442
|
+
let responseData;
|
|
443
|
+
let responseHeaders = null;
|
|
444
|
+
try {
|
|
445
|
+
// Try to capture response data
|
|
446
|
+
responseData = this.responseText || this.response;
|
|
447
|
+
}
|
|
448
|
+
catch (e) {
|
|
449
|
+
// Response might not be accessible in some cases
|
|
450
|
+
responseData = null;
|
|
451
|
+
}
|
|
452
|
+
// Capture response headers
|
|
453
|
+
try {
|
|
454
|
+
responseHeaders = {};
|
|
455
|
+
const allHeaders = this.getAllResponseHeaders();
|
|
456
|
+
if (allHeaders) {
|
|
457
|
+
// Parse headers string into object
|
|
458
|
+
allHeaders.split('\r\n').forEach(line => {
|
|
459
|
+
const parts = line.split(': ');
|
|
460
|
+
if (parts.length === 2) {
|
|
461
|
+
responseHeaders[parts[0]] = parts[1];
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch (e) {
|
|
467
|
+
if (DEBUG) {
|
|
468
|
+
console.warn("[Sailfish] Failed to capture XHR response headers:", e);
|
|
469
|
+
}
|
|
470
|
+
responseHeaders = null;
|
|
471
|
+
}
|
|
386
472
|
if (status >= 200 && status < 300) {
|
|
387
|
-
emitFinished(true, status, "");
|
|
473
|
+
emitFinished(true, status, "", responseData, responseHeaders);
|
|
388
474
|
}
|
|
389
475
|
else {
|
|
390
476
|
const msg = this.statusText || `HTTP ${status}`;
|
|
391
|
-
emitFinished(false, status, msg);
|
|
477
|
+
emitFinished(false, status, msg, responseData, responseHeaders);
|
|
392
478
|
}
|
|
393
479
|
}, { once: true });
|
|
394
480
|
// 7️⃣ On network/CORS error
|
|
@@ -435,21 +521,74 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
435
521
|
return injectHeaderWrapper(target, thisArg, args, input, init, sessionId, url);
|
|
436
522
|
},
|
|
437
523
|
});
|
|
438
|
-
// 2️⃣ Fetch interceptor
|
|
524
|
+
// 2️⃣ Fetch interceptor's injectHeaderWrapper(); emits 'networkRequest' event
|
|
439
525
|
async function injectHeaderWrapper(target, thisArg, args, input, init, sessionId, url) {
|
|
440
526
|
if (!sessionId) {
|
|
441
527
|
return target.apply(thisArg, args);
|
|
442
528
|
}
|
|
443
|
-
|
|
529
|
+
// Generate a fresh UUID for this request attempt
|
|
530
|
+
let networkUUID = uuidv4();
|
|
444
531
|
const urlAndStoredUuids = getUrlAndStoredUuids();
|
|
445
532
|
const method = init.method || "GET";
|
|
446
533
|
const startTime = Date.now();
|
|
534
|
+
// Capture request headers and body
|
|
535
|
+
let requestHeaders = {};
|
|
536
|
+
let requestBody;
|
|
537
|
+
try {
|
|
538
|
+
if (input instanceof Request) {
|
|
539
|
+
// Extract headers from Request object
|
|
540
|
+
input.headers.forEach((value, key) => {
|
|
541
|
+
requestHeaders[key] = value;
|
|
542
|
+
});
|
|
543
|
+
// Try to clone and read body if present
|
|
544
|
+
try {
|
|
545
|
+
const clonedRequest = input.clone();
|
|
546
|
+
requestBody = await clonedRequest.text();
|
|
547
|
+
}
|
|
548
|
+
catch (e) {
|
|
549
|
+
requestBody = null;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
// Extract headers from init
|
|
554
|
+
if (init.headers) {
|
|
555
|
+
if (init.headers instanceof Headers) {
|
|
556
|
+
init.headers.forEach((value, key) => {
|
|
557
|
+
requestHeaders[key] = value;
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
else if (Array.isArray(init.headers)) {
|
|
561
|
+
init.headers.forEach(([key, value]) => {
|
|
562
|
+
requestHeaders[key] = value;
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
requestHeaders = { ...init.headers };
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// Capture request body
|
|
570
|
+
requestBody = init.body;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
catch (e) {
|
|
574
|
+
if (DEBUG) {
|
|
575
|
+
console.warn("[Sailfish] Failed to capture request data:", e);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// Filter out internal Sailfish headers from captured headers
|
|
579
|
+
delete requestHeaders[xSf3RidHeader];
|
|
580
|
+
const funcSpanHeaderName = getFuncSpanHeader()?.name;
|
|
581
|
+
if (funcSpanHeaderName) {
|
|
582
|
+
delete requestHeaders[funcSpanHeaderName];
|
|
583
|
+
}
|
|
447
584
|
try {
|
|
448
585
|
let response = await injectHeader(target, thisArg, input, init, sessionId, urlAndStoredUuids.page_visit_uuid, networkUUID);
|
|
449
586
|
let isRetry = false;
|
|
450
587
|
// Retry logic for 400/403 before logging finished event
|
|
451
588
|
if (BAD_HTTP_STATUS.includes(response.status)) {
|
|
452
589
|
DEBUG && console.log("Perform retry as status was fail:", response);
|
|
590
|
+
// Generate a NEW UUID for the retry request so each request has a unique ID
|
|
591
|
+
networkUUID = uuidv4();
|
|
453
592
|
response = await retryWithoutPropagateHeaders(target, thisArg, args, url);
|
|
454
593
|
isRetry = true;
|
|
455
594
|
}
|
|
@@ -457,6 +596,33 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
457
596
|
const status = response.status;
|
|
458
597
|
const success = response.ok;
|
|
459
598
|
const error = success ? "" : `Request Error: ${response.statusText}`;
|
|
599
|
+
// Capture response data
|
|
600
|
+
let responseData;
|
|
601
|
+
try {
|
|
602
|
+
// Clone the response so we don't consume the original stream
|
|
603
|
+
const clonedResponse = response.clone();
|
|
604
|
+
responseData = await clonedResponse.text();
|
|
605
|
+
}
|
|
606
|
+
catch (e) {
|
|
607
|
+
if (DEBUG) {
|
|
608
|
+
console.warn("[Sailfish] Failed to capture response data:", e);
|
|
609
|
+
}
|
|
610
|
+
responseData = null;
|
|
611
|
+
}
|
|
612
|
+
// Capture response headers
|
|
613
|
+
let responseHeaders = null;
|
|
614
|
+
try {
|
|
615
|
+
responseHeaders = {};
|
|
616
|
+
response.headers.forEach((value, key) => {
|
|
617
|
+
responseHeaders[key] = value;
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
catch (e) {
|
|
621
|
+
if (DEBUG) {
|
|
622
|
+
console.warn("[Sailfish] Failed to capture response headers:", e);
|
|
623
|
+
}
|
|
624
|
+
responseHeaders = null;
|
|
625
|
+
}
|
|
460
626
|
// Emit 'networkRequestFinished' event
|
|
461
627
|
const eventData = {
|
|
462
628
|
type: NetworkRequestEventId,
|
|
@@ -473,6 +639,10 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
473
639
|
method,
|
|
474
640
|
url,
|
|
475
641
|
retry_without_trace_id: isRetry,
|
|
642
|
+
request_headers: requestHeaders,
|
|
643
|
+
request_body: requestBody,
|
|
644
|
+
response_headers: responseHeaders,
|
|
645
|
+
response_body: responseData,
|
|
476
646
|
},
|
|
477
647
|
...urlAndStoredUuids,
|
|
478
648
|
};
|
|
@@ -503,6 +673,9 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
503
673
|
error: errorMessage,
|
|
504
674
|
method,
|
|
505
675
|
url,
|
|
676
|
+
request_headers: requestHeaders,
|
|
677
|
+
request_body: requestBody,
|
|
678
|
+
response_body: null,
|
|
506
679
|
},
|
|
507
680
|
...urlAndStoredUuids,
|
|
508
681
|
};
|
|
@@ -512,11 +685,23 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
512
685
|
}
|
|
513
686
|
// Helper function to inject the X-Sf3-Rid header
|
|
514
687
|
async function injectHeader(target, thisArg, input, init, sessionId, pageVisitUUID, networkUUID) {
|
|
688
|
+
// Get funcspan header if tracking is enabled
|
|
689
|
+
const funcSpanHeader = getFuncSpanHeader();
|
|
515
690
|
if (input instanceof Request) {
|
|
516
691
|
// Clone the Request and modify headers
|
|
517
692
|
const clonedRequest = input.clone();
|
|
518
693
|
const newHeaders = new Headers(clonedRequest.headers);
|
|
519
694
|
newHeaders.set(xSf3RidHeader, `${sessionId}/${pageVisitUUID}/${networkUUID}`);
|
|
695
|
+
// Add funcspan header if tracking is enabled
|
|
696
|
+
if (funcSpanHeader) {
|
|
697
|
+
newHeaders.set(funcSpanHeader.name, funcSpanHeader.value);
|
|
698
|
+
if (DEBUG) {
|
|
699
|
+
console.log(`[Sailfish] Added funcspan header to HTTP Request:`, {
|
|
700
|
+
url: input.url,
|
|
701
|
+
header: funcSpanHeader.name,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
}
|
|
520
705
|
const modifiedRequest = new Request(clonedRequest, {
|
|
521
706
|
headers: newHeaders,
|
|
522
707
|
});
|
|
@@ -527,6 +712,16 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
527
712
|
const modifiedInit = { ...init };
|
|
528
713
|
const newHeaders = new Headers(init.headers || {});
|
|
529
714
|
newHeaders.set(xSf3RidHeader, `${sessionId}/${pageVisitUUID}/${networkUUID}`);
|
|
715
|
+
// Add funcspan header if tracking is enabled
|
|
716
|
+
if (funcSpanHeader) {
|
|
717
|
+
newHeaders.set(funcSpanHeader.name, funcSpanHeader.value);
|
|
718
|
+
if (DEBUG) {
|
|
719
|
+
console.log(`[Sailfish] Added funcspan header to HTTP fetch:`, {
|
|
720
|
+
url: typeof input === "string" ? input : input.href,
|
|
721
|
+
header: funcSpanHeader.name,
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
}
|
|
530
725
|
modifiedInit.headers = newHeaders;
|
|
531
726
|
return await target.call(thisArg, input, modifiedInit);
|
|
532
727
|
}
|
|
@@ -673,6 +868,8 @@ export const initRecorder = async (options) => {
|
|
|
673
868
|
return;
|
|
674
869
|
const g = (window.__sailfish_recorder ||= {});
|
|
675
870
|
const currentSessionId = getOrSetSessionId();
|
|
871
|
+
// remove stale page visit data from previous sessions
|
|
872
|
+
clearPageVisitDataFromSessionStorage();
|
|
676
873
|
// If already initialized for this session and socket is open, do nothing.
|
|
677
874
|
if (g.initialized &&
|
|
678
875
|
g.sessionId === currentSessionId &&
|