@sailfish-ai/recorder 1.8.2 → 1.8.7
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 +194 -11
- package/dist/recorder.cjs +946 -802
- package/dist/recorder.js +1032 -884
- package/dist/recorder.js.br +0 -0
- package/dist/recorder.js.gz +0 -0
- package/dist/recorder.umd.cjs +947 -803
- 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,31 @@ 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
|
|
359
406
|
// 4️⃣ Helper to emit networkRequestFinished
|
|
360
|
-
const emitFinished = (success, status, errorMsg) => {
|
|
407
|
+
const emitFinished = (success, status, errorMsg, responseData, responseHeaders) => {
|
|
361
408
|
if (finished)
|
|
362
409
|
return;
|
|
363
410
|
finished = true;
|
|
@@ -376,6 +423,10 @@ function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo = []) {
|
|
|
376
423
|
error: errorMsg,
|
|
377
424
|
method: this._requestMethod,
|
|
378
425
|
url,
|
|
426
|
+
request_headers: requestHeaders,
|
|
427
|
+
request_body: requestBody,
|
|
428
|
+
response_headers: responseHeaders,
|
|
429
|
+
response_body: responseData,
|
|
379
430
|
},
|
|
380
431
|
...getUrlAndStoredUuids(),
|
|
381
432
|
});
|
|
@@ -383,12 +434,42 @@ function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo = []) {
|
|
|
383
434
|
// 6️⃣ On successful load
|
|
384
435
|
this.addEventListener("load", () => {
|
|
385
436
|
const status = this.status || 0;
|
|
437
|
+
let responseData;
|
|
438
|
+
let responseHeaders = null;
|
|
439
|
+
try {
|
|
440
|
+
// Try to capture response data
|
|
441
|
+
responseData = this.responseText || this.response;
|
|
442
|
+
}
|
|
443
|
+
catch (e) {
|
|
444
|
+
// Response might not be accessible in some cases
|
|
445
|
+
responseData = null;
|
|
446
|
+
}
|
|
447
|
+
// Capture response headers
|
|
448
|
+
try {
|
|
449
|
+
responseHeaders = {};
|
|
450
|
+
const allHeaders = this.getAllResponseHeaders();
|
|
451
|
+
if (allHeaders) {
|
|
452
|
+
// Parse headers string into object
|
|
453
|
+
allHeaders.split('\r\n').forEach(line => {
|
|
454
|
+
const parts = line.split(': ');
|
|
455
|
+
if (parts.length === 2) {
|
|
456
|
+
responseHeaders[parts[0]] = parts[1];
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
catch (e) {
|
|
462
|
+
if (DEBUG) {
|
|
463
|
+
console.warn("[Sailfish] Failed to capture XHR response headers:", e);
|
|
464
|
+
}
|
|
465
|
+
responseHeaders = null;
|
|
466
|
+
}
|
|
386
467
|
if (status >= 200 && status < 300) {
|
|
387
|
-
emitFinished(true, status, "");
|
|
468
|
+
emitFinished(true, status, "", responseData, responseHeaders);
|
|
388
469
|
}
|
|
389
470
|
else {
|
|
390
471
|
const msg = this.statusText || `HTTP ${status}`;
|
|
391
|
-
emitFinished(false, status, msg);
|
|
472
|
+
emitFinished(false, status, msg, responseData, responseHeaders);
|
|
392
473
|
}
|
|
393
474
|
}, { once: true });
|
|
394
475
|
// 7️⃣ On network/CORS error
|
|
@@ -435,7 +516,7 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
435
516
|
return injectHeaderWrapper(target, thisArg, args, input, init, sessionId, url);
|
|
436
517
|
},
|
|
437
518
|
});
|
|
438
|
-
// 2️⃣ Fetch interceptor
|
|
519
|
+
// 2️⃣ Fetch interceptor's injectHeaderWrapper(); emits 'networkRequest' event
|
|
439
520
|
async function injectHeaderWrapper(target, thisArg, args, input, init, sessionId, url) {
|
|
440
521
|
if (!sessionId) {
|
|
441
522
|
return target.apply(thisArg, args);
|
|
@@ -444,6 +525,50 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
444
525
|
const urlAndStoredUuids = getUrlAndStoredUuids();
|
|
445
526
|
const method = init.method || "GET";
|
|
446
527
|
const startTime = Date.now();
|
|
528
|
+
// Capture request headers and body
|
|
529
|
+
let requestHeaders = {};
|
|
530
|
+
let requestBody;
|
|
531
|
+
try {
|
|
532
|
+
if (input instanceof Request) {
|
|
533
|
+
// Extract headers from Request object
|
|
534
|
+
input.headers.forEach((value, key) => {
|
|
535
|
+
requestHeaders[key] = value;
|
|
536
|
+
});
|
|
537
|
+
// Try to clone and read body if present
|
|
538
|
+
try {
|
|
539
|
+
const clonedRequest = input.clone();
|
|
540
|
+
requestBody = await clonedRequest.text();
|
|
541
|
+
}
|
|
542
|
+
catch (e) {
|
|
543
|
+
requestBody = null;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
// Extract headers from init
|
|
548
|
+
if (init.headers) {
|
|
549
|
+
if (init.headers instanceof Headers) {
|
|
550
|
+
init.headers.forEach((value, key) => {
|
|
551
|
+
requestHeaders[key] = value;
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
else if (Array.isArray(init.headers)) {
|
|
555
|
+
init.headers.forEach(([key, value]) => {
|
|
556
|
+
requestHeaders[key] = value;
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
requestHeaders = { ...init.headers };
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
// Capture request body
|
|
564
|
+
requestBody = init.body;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
catch (e) {
|
|
568
|
+
if (DEBUG) {
|
|
569
|
+
console.warn("[Sailfish] Failed to capture request data:", e);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
447
572
|
try {
|
|
448
573
|
let response = await injectHeader(target, thisArg, input, init, sessionId, urlAndStoredUuids.page_visit_uuid, networkUUID);
|
|
449
574
|
let isRetry = false;
|
|
@@ -457,6 +582,33 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
457
582
|
const status = response.status;
|
|
458
583
|
const success = response.ok;
|
|
459
584
|
const error = success ? "" : `Request Error: ${response.statusText}`;
|
|
585
|
+
// Capture response data
|
|
586
|
+
let responseData;
|
|
587
|
+
try {
|
|
588
|
+
// Clone the response so we don't consume the original stream
|
|
589
|
+
const clonedResponse = response.clone();
|
|
590
|
+
responseData = await clonedResponse.text();
|
|
591
|
+
}
|
|
592
|
+
catch (e) {
|
|
593
|
+
if (DEBUG) {
|
|
594
|
+
console.warn("[Sailfish] Failed to capture response data:", e);
|
|
595
|
+
}
|
|
596
|
+
responseData = null;
|
|
597
|
+
}
|
|
598
|
+
// Capture response headers
|
|
599
|
+
let responseHeaders = null;
|
|
600
|
+
try {
|
|
601
|
+
responseHeaders = {};
|
|
602
|
+
response.headers.forEach((value, key) => {
|
|
603
|
+
responseHeaders[key] = value;
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
catch (e) {
|
|
607
|
+
if (DEBUG) {
|
|
608
|
+
console.warn("[Sailfish] Failed to capture response headers:", e);
|
|
609
|
+
}
|
|
610
|
+
responseHeaders = null;
|
|
611
|
+
}
|
|
460
612
|
// Emit 'networkRequestFinished' event
|
|
461
613
|
const eventData = {
|
|
462
614
|
type: NetworkRequestEventId,
|
|
@@ -473,6 +625,10 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
473
625
|
method,
|
|
474
626
|
url,
|
|
475
627
|
retry_without_trace_id: isRetry,
|
|
628
|
+
request_headers: requestHeaders,
|
|
629
|
+
request_body: requestBody,
|
|
630
|
+
response_headers: responseHeaders,
|
|
631
|
+
response_body: responseData,
|
|
476
632
|
},
|
|
477
633
|
...urlAndStoredUuids,
|
|
478
634
|
};
|
|
@@ -503,6 +659,9 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
503
659
|
error: errorMessage,
|
|
504
660
|
method,
|
|
505
661
|
url,
|
|
662
|
+
request_headers: requestHeaders,
|
|
663
|
+
request_body: requestBody,
|
|
664
|
+
response_body: null,
|
|
506
665
|
},
|
|
507
666
|
...urlAndStoredUuids,
|
|
508
667
|
};
|
|
@@ -512,11 +671,23 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
512
671
|
}
|
|
513
672
|
// Helper function to inject the X-Sf3-Rid header
|
|
514
673
|
async function injectHeader(target, thisArg, input, init, sessionId, pageVisitUUID, networkUUID) {
|
|
674
|
+
// Get funcspan header if tracking is enabled
|
|
675
|
+
const funcSpanHeader = getFuncSpanHeader();
|
|
515
676
|
if (input instanceof Request) {
|
|
516
677
|
// Clone the Request and modify headers
|
|
517
678
|
const clonedRequest = input.clone();
|
|
518
679
|
const newHeaders = new Headers(clonedRequest.headers);
|
|
519
680
|
newHeaders.set(xSf3RidHeader, `${sessionId}/${pageVisitUUID}/${networkUUID}`);
|
|
681
|
+
// Add funcspan header if tracking is enabled
|
|
682
|
+
if (funcSpanHeader) {
|
|
683
|
+
newHeaders.set(funcSpanHeader.name, funcSpanHeader.value);
|
|
684
|
+
if (DEBUG) {
|
|
685
|
+
console.log(`[Sailfish] Added funcspan header to HTTP Request:`, {
|
|
686
|
+
url: input.url,
|
|
687
|
+
header: funcSpanHeader.name,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
}
|
|
520
691
|
const modifiedRequest = new Request(clonedRequest, {
|
|
521
692
|
headers: newHeaders,
|
|
522
693
|
});
|
|
@@ -527,6 +698,16 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
527
698
|
const modifiedInit = { ...init };
|
|
528
699
|
const newHeaders = new Headers(init.headers || {});
|
|
529
700
|
newHeaders.set(xSf3RidHeader, `${sessionId}/${pageVisitUUID}/${networkUUID}`);
|
|
701
|
+
// Add funcspan header if tracking is enabled
|
|
702
|
+
if (funcSpanHeader) {
|
|
703
|
+
newHeaders.set(funcSpanHeader.name, funcSpanHeader.value);
|
|
704
|
+
if (DEBUG) {
|
|
705
|
+
console.log(`[Sailfish] Added funcspan header to HTTP fetch:`, {
|
|
706
|
+
url: typeof input === "string" ? input : input.href,
|
|
707
|
+
header: funcSpanHeader.name,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
}
|
|
530
711
|
modifiedInit.headers = newHeaders;
|
|
531
712
|
return await target.call(thisArg, input, modifiedInit);
|
|
532
713
|
}
|
|
@@ -673,6 +854,8 @@ export const initRecorder = async (options) => {
|
|
|
673
854
|
return;
|
|
674
855
|
const g = (window.__sailfish_recorder ||= {});
|
|
675
856
|
const currentSessionId = getOrSetSessionId();
|
|
857
|
+
// remove stale page visit data from previous sessions
|
|
858
|
+
clearPageVisitDataFromSessionStorage();
|
|
676
859
|
// If already initialized for this session and socket is open, do nothing.
|
|
677
860
|
if (g.initialized &&
|
|
678
861
|
g.sessionId === currentSessionId &&
|