@sailfish-ai/recorder 1.8.21 → 1.9.0

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.
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
+ const e = /* @__PURE__ */ new Set(["button", "a", "input", "select", "textarea", "details", "summary", "Button", "Link", "Input", "Select", "Checkbox", "Radio", "Switch", "Tab", "MenuItem", "IconButton", "TextField", "TextArea"]), t = /* @__PURE__ */ new Set(["button", "link", "checkbox", "radio", "switch", "tab", "menuitem", "option", "combobox", "textbox", "searchbox", "slider", "spinbutton"]);
4
+ function sailfishSourcePlugin(i) {
5
+ const { types: n } = i;
6
+ return { name: "sailfish-source", visitor: { JSXOpeningElement(i2, r) {
7
+ const { opts: s = {}, filename: o } = r, { allElements: a = false, rootDir: u = process.cwd() } = s;
8
+ if (!o) return;
9
+ const l = i2.node.name;
10
+ let c;
11
+ if (n.isJSXIdentifier(l)) c = l.name;
12
+ else {
13
+ if (!n.isJSXMemberExpression(l)) return;
14
+ c = n.isJSXIdentifier(l.property) ? l.property.name : "";
15
+ }
16
+ if (!a) {
17
+ const r2 = (function isInteractiveElement(i3, n2, r3) {
18
+ if (e.has(n2)) return true;
19
+ n2[0], n2[0].toLowerCase();
20
+ for (const e2 of i3.node.attributes) {
21
+ if (!r3.isJSXAttribute(e2) || !r3.isJSXIdentifier(e2.name)) continue;
22
+ const i4 = e2.name.name;
23
+ if ("onClick" === i4 || "onPress" === i4) return true;
24
+ if ("data-testid" === i4) return true;
25
+ if ("role" === i4 && r3.isStringLiteral(e2.value) && t.has(e2.value.value.toLowerCase())) return true;
26
+ if ("tabIndex" === i4) return true;
27
+ if ("href" === i4 || "to" === i4) return true;
28
+ }
29
+ return false;
30
+ })(i2, c, n);
31
+ if (!r2) return;
32
+ }
33
+ if (i2.node.attributes.some((e2) => n.isJSXAttribute(e2) && n.isJSXIdentifier(e2.name) && "data-sf-source" === e2.name.name)) return;
34
+ const f = i2.node.loc;
35
+ if (!f) return;
36
+ let d = o;
37
+ try {
38
+ d = (void 0)(u, o), d = d.replace(/\\/g, "/");
39
+ } catch {
40
+ }
41
+ const m = `${d}:${f.start.line}`, b = n.jsxAttribute(n.jsxIdentifier("data-sf-source"), n.stringLiteral(m));
42
+ i2.node.attributes.push(b);
43
+ } } };
44
+ }
45
+ exports.default = sailfishSourcePlugin, exports.sailfishSourcePlugin = sailfishSourcePlugin;
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Babel Plugin: Sailfish Source Injection
3
+ *
4
+ * Injects data-sf-source attributes into JSX elements at compile time.
5
+ * This provides source file/line information for coverage analysis.
6
+ *
7
+ * Usage in babel.config.js:
8
+ *
9
+ * const sailfishSourcePlugin = require('@anthropic/veritas/babel-plugin-sailfish-source');
10
+ *
11
+ * module.exports = {
12
+ * plugins: [
13
+ * [sailfishSourcePlugin, {
14
+ * // Options:
15
+ * // - allElements: Add to all elements (default: false, only interactive)
16
+ * // - rootDir: Strip this prefix from file paths (default: process.cwd())
17
+ * }]
18
+ * ]
19
+ * };
20
+ *
21
+ * Or in vite.config.ts:
22
+ *
23
+ * import sailfishSourcePlugin from '@anthropic/veritas/babel-plugin-sailfish-source';
24
+ *
25
+ * export default defineConfig({
26
+ * plugins: [
27
+ * react({
28
+ * babel: {
29
+ * plugins: [sailfishSourcePlugin]
30
+ * }
31
+ * })
32
+ * ]
33
+ * });
34
+ */
35
+ import * as path from "path";
36
+ // Interactive element tag names that get source info by default
37
+ const INTERACTIVE_TAGS = new Set([
38
+ "button",
39
+ "a",
40
+ "input",
41
+ "select",
42
+ "textarea",
43
+ "details",
44
+ "summary",
45
+ // Also include common component patterns
46
+ "Button",
47
+ "Link",
48
+ "Input",
49
+ "Select",
50
+ "Checkbox",
51
+ "Radio",
52
+ "Switch",
53
+ "Tab",
54
+ "MenuItem",
55
+ "IconButton",
56
+ "TextField",
57
+ "TextArea",
58
+ ]);
59
+ // Interactive role values
60
+ const INTERACTIVE_ROLES = new Set([
61
+ "button",
62
+ "link",
63
+ "checkbox",
64
+ "radio",
65
+ "switch",
66
+ "tab",
67
+ "menuitem",
68
+ "option",
69
+ "combobox",
70
+ "textbox",
71
+ "searchbox",
72
+ "slider",
73
+ "spinbutton",
74
+ ]);
75
+ export default function sailfishSourcePlugin(babel) {
76
+ const { types: t } = babel;
77
+ return {
78
+ name: "sailfish-source",
79
+ visitor: {
80
+ JSXOpeningElement(nodePath, state) {
81
+ const { opts = {}, filename } = state;
82
+ const { allElements = false, rootDir = process.cwd() } = opts;
83
+ // Skip if no filename available
84
+ if (!filename) {
85
+ return;
86
+ }
87
+ // Get element name
88
+ const nameNode = nodePath.node.name;
89
+ let tagName;
90
+ if (t.isJSXIdentifier(nameNode)) {
91
+ tagName = nameNode.name;
92
+ }
93
+ else if (t.isJSXMemberExpression(nameNode)) {
94
+ // e.g., MUI.Button -> Button
95
+ tagName = t.isJSXIdentifier(nameNode.property)
96
+ ? nameNode.property.name
97
+ : "";
98
+ }
99
+ else {
100
+ return;
101
+ }
102
+ // Check if element should get source info
103
+ if (!allElements) {
104
+ const isInteractive = isInteractiveElement(nodePath, tagName, t);
105
+ if (!isInteractive) {
106
+ return;
107
+ }
108
+ }
109
+ // Check if already has data-sf-source
110
+ const hasSourceAttr = nodePath.node.attributes.some((attr) => t.isJSXAttribute(attr) &&
111
+ t.isJSXIdentifier(attr.name) &&
112
+ attr.name.name === "data-sf-source");
113
+ if (hasSourceAttr) {
114
+ return;
115
+ }
116
+ // Get source location
117
+ const loc = nodePath.node.loc;
118
+ if (!loc) {
119
+ return;
120
+ }
121
+ // Create relative file path
122
+ let relativePath = filename;
123
+ try {
124
+ relativePath = path.relative(rootDir, filename);
125
+ // Normalize to forward slashes
126
+ relativePath = relativePath.replace(/\\/g, "/");
127
+ }
128
+ catch {
129
+ // Keep absolute path if relative fails
130
+ }
131
+ // Create source string: "file:line"
132
+ const sourceValue = `${relativePath}:${loc.start.line}`;
133
+ // Add data-sf-source attribute
134
+ const sourceAttr = t.jsxAttribute(t.jsxIdentifier("data-sf-source"), t.stringLiteral(sourceValue));
135
+ nodePath.node.attributes.push(sourceAttr);
136
+ },
137
+ },
138
+ };
139
+ }
140
+ /**
141
+ * Check if element is interactive based on tag name, attributes, or role.
142
+ */
143
+ function isInteractiveElement(nodePath, tagName, t) {
144
+ // Check tag name
145
+ if (INTERACTIVE_TAGS.has(tagName)) {
146
+ return true;
147
+ }
148
+ // Check for lowercase tags (HTML elements)
149
+ const isLowerCase = tagName[0] === tagName[0].toLowerCase();
150
+ // Check attributes
151
+ for (const attr of nodePath.node.attributes) {
152
+ if (!t.isJSXAttribute(attr) || !t.isJSXIdentifier(attr.name)) {
153
+ continue;
154
+ }
155
+ const attrName = attr.name.name;
156
+ // Has onClick handler
157
+ if (attrName === "onClick" || attrName === "onPress") {
158
+ return true;
159
+ }
160
+ // Has data-testid (likely important for testing)
161
+ if (attrName === "data-testid") {
162
+ return true;
163
+ }
164
+ // Has role attribute with interactive value
165
+ if (attrName === "role" && t.isStringLiteral(attr.value)) {
166
+ if (INTERACTIVE_ROLES.has(attr.value.value.toLowerCase())) {
167
+ return true;
168
+ }
169
+ }
170
+ // Has tabIndex (focusable)
171
+ if (attrName === "tabIndex") {
172
+ return true;
173
+ }
174
+ // Has href (link-like)
175
+ if (attrName === "href" || attrName === "to") {
176
+ return true;
177
+ }
178
+ }
179
+ return false;
180
+ }
181
+ // Also export as named export for ESM
182
+ export { sailfishSourcePlugin };
@@ -0,0 +1,319 @@
1
+ /**
2
+ * React Fiber Hook Implementation
3
+ *
4
+ * Hooks into React's internals using the same mechanism as React DevTools.
5
+ * Adds data-sf-component attributes to DOM elements for coverage correlation.
6
+ * These attributes are captured by rrweb and extracted during on-demand analysis.
7
+ *
8
+ * NOTE: React 19 removed _debugSource, so we use _debugOwner to get component names.
9
+ * Source file/line information is no longer available directly from fibers.
10
+ */
11
+ import { FiberTag } from "./fiberTypes";
12
+ // Debug flag - enable for troubleshooting
13
+ const DEBUG = false;
14
+ // Track elements we've already annotated to avoid re-processing
15
+ const annotatedElements = new WeakSet();
16
+ // Flag to track if hook is installed
17
+ let hookInstalled = false;
18
+ // Store reference to our handler for chaining
19
+ let ourCommitHandler = null;
20
+ let originalCommitHandler = null;
21
+ // Check if we're in development mode
22
+ const IS_DEV = process.env.NODE_ENV !== "production";
23
+ /**
24
+ * Get the display name for a component from its Fiber.
25
+ */
26
+ function getDisplayName(fiber) {
27
+ const type = fiber.type;
28
+ if (!type) {
29
+ return "";
30
+ }
31
+ // Function or class component
32
+ if (typeof type === "function") {
33
+ return type.displayName || type.name || "";
34
+ }
35
+ // ForwardRef
36
+ if (typeof type === "object" && type !== null) {
37
+ if (type.$$typeof) {
38
+ // React.forwardRef
39
+ if (type.render) {
40
+ return type.render.displayName || type.render.name || "";
41
+ }
42
+ // React.memo
43
+ if (type.type) {
44
+ return getDisplayNameFromType(type.type);
45
+ }
46
+ }
47
+ }
48
+ // Host component (DOM element) - don't return tag name
49
+ if (typeof type === "string") {
50
+ return "";
51
+ }
52
+ return "";
53
+ }
54
+ /**
55
+ * Helper to get display name from a type object.
56
+ */
57
+ function getDisplayNameFromType(type) {
58
+ if (typeof type === "function") {
59
+ return type.displayName || type.name || "";
60
+ }
61
+ return "";
62
+ }
63
+ /**
64
+ * Get component names from _debugOwner chain.
65
+ * Returns array from nearest owner to root (e.g., ["Button", "Sidebar", "App"])
66
+ */
67
+ function getOwnerChain(fiber) {
68
+ const owners = [];
69
+ let owner = fiber?._debugOwner;
70
+ while (owner) {
71
+ const name = getDisplayName(owner);
72
+ if (name && !owners.includes(name)) {
73
+ owners.push(name);
74
+ }
75
+ owner = owner._debugOwner;
76
+ }
77
+ return owners;
78
+ }
79
+ /**
80
+ * Find the nearest named component by walking up _debugOwner chain.
81
+ */
82
+ function findNearestComponentName(fiber) {
83
+ // First check the fiber itself
84
+ const selfName = fiber ? getDisplayName(fiber) : "";
85
+ if (selfName) {
86
+ return selfName;
87
+ }
88
+ // Then check _debugOwner chain
89
+ let owner = fiber?._debugOwner;
90
+ while (owner) {
91
+ const name = getDisplayName(owner);
92
+ if (name) {
93
+ return name;
94
+ }
95
+ owner = owner._debugOwner;
96
+ }
97
+ return null;
98
+ }
99
+ /**
100
+ * Get the React Fiber node attached to a DOM element.
101
+ */
102
+ function getFiberFromElement(element) {
103
+ const keys = Object.keys(element);
104
+ // React 18+ uses __reactFiber$
105
+ const fiberKey = keys.find((key) => key.startsWith("__reactFiber$"));
106
+ if (fiberKey) {
107
+ return element[fiberKey];
108
+ }
109
+ // Older React versions use __reactInternalInstance$
110
+ const internalKey = keys.find((key) => key.startsWith("__reactInternalInstance$"));
111
+ if (internalKey) {
112
+ return element[internalKey];
113
+ }
114
+ return null;
115
+ }
116
+ /**
117
+ * Add component attributes to a DOM element.
118
+ *
119
+ * Attributes added:
120
+ * - data-sf-component: Nearest owner component name (e.g., "Button")
121
+ * - data-sf-owners: Full owner chain (e.g., "Button,Sidebar,App")
122
+ */
123
+ function annotateElement(element, fiber) {
124
+ if (annotatedElements.has(element)) {
125
+ return false;
126
+ }
127
+ // Get component name from owner chain
128
+ const componentName = findNearestComponentName(fiber);
129
+ if (!componentName) {
130
+ return false;
131
+ }
132
+ // Get full owner chain for more context
133
+ const ownerChain = getOwnerChain(fiber);
134
+ try {
135
+ // Add component name
136
+ element.setAttribute("data-sf-component", componentName);
137
+ // Add owner chain if we have multiple owners
138
+ if (ownerChain.length > 0) {
139
+ element.setAttribute("data-sf-owners", ownerChain.join(","));
140
+ }
141
+ annotatedElements.add(element);
142
+ if (DEBUG) {
143
+ console.log(`[Sailfish Fiber] Annotated ${element.tagName} with component: ${componentName}, owners: ${ownerChain.join(" > ")}`);
144
+ }
145
+ return true;
146
+ }
147
+ catch (e) {
148
+ // Ignore errors (e.g., SVG elements may not support setAttribute)
149
+ return false;
150
+ }
151
+ }
152
+ /**
153
+ * Process a single DOM element - find its fiber and add component attributes.
154
+ */
155
+ function processElement(element) {
156
+ if (annotatedElements.has(element)) {
157
+ return false;
158
+ }
159
+ const fiber = getFiberFromElement(element);
160
+ if (!fiber) {
161
+ return false;
162
+ }
163
+ return annotateElement(element, fiber);
164
+ }
165
+ /**
166
+ * Process all DOM elements in a fiber's subtree.
167
+ */
168
+ function processFiberDOM(fiber) {
169
+ let count = 0;
170
+ // If this fiber has a direct DOM node
171
+ if (fiber.tag === FiberTag.HostComponent &&
172
+ fiber.stateNode instanceof HTMLElement) {
173
+ if (annotateElement(fiber.stateNode, fiber)) {
174
+ count++;
175
+ }
176
+ }
177
+ // Process children
178
+ let child = fiber.child;
179
+ while (child) {
180
+ count += processFiberDOM(child);
181
+ child = child.sibling;
182
+ }
183
+ return count;
184
+ }
185
+ /**
186
+ * Process a commit, adding component attributes to DOM elements.
187
+ */
188
+ function processCommit(root) {
189
+ let processedCount = 0;
190
+ try {
191
+ const currentFiber = root.current;
192
+ if (!currentFiber) {
193
+ return;
194
+ }
195
+ processedCount = processFiberDOM(currentFiber);
196
+ }
197
+ catch (error) {
198
+ // Never break React - log and continue
199
+ console.warn("[Sailfish] Error processing fiber commit:", error);
200
+ return;
201
+ }
202
+ if (DEBUG && processedCount > 0) {
203
+ console.log(`[Sailfish Fiber] Commit processed: ${processedCount} elements annotated`);
204
+ }
205
+ }
206
+ /**
207
+ * Create our commit handler that processes commits and chains to original.
208
+ */
209
+ function createCommitHandler(existingHook) {
210
+ return function onCommitFiberRoot(rendererID, root, priorityLevel) {
211
+ try {
212
+ processCommit(root);
213
+ }
214
+ catch (error) {
215
+ console.warn("[Sailfish] Error in onCommitFiberRoot:", error);
216
+ }
217
+ // Call original if it exists
218
+ if (originalCommitHandler) {
219
+ try {
220
+ originalCommitHandler.call(existingHook, rendererID, root, priorityLevel);
221
+ }
222
+ catch (error) {
223
+ console.warn("[Sailfish] Error calling original onCommitFiberRoot:", error);
224
+ }
225
+ }
226
+ };
227
+ }
228
+ /**
229
+ * Install the Fiber hook.
230
+ *
231
+ * NOTE: For best results, this should be called BEFORE React loads.
232
+ * If React has already loaded, call processExistingTree() after.
233
+ *
234
+ * @returns true if hook was installed, false if already installed or failed
235
+ */
236
+ export function installFiberHook() {
237
+ if (hookInstalled) {
238
+ return false;
239
+ }
240
+ if (typeof window === "undefined") {
241
+ return false;
242
+ }
243
+ try {
244
+ const existingHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
245
+ if (existingHook) {
246
+ // Save original commit handler
247
+ originalCommitHandler =
248
+ existingHook.onCommitFiberRoot?.bind(existingHook) || null;
249
+ // Create our handler
250
+ ourCommitHandler = createCommitHandler(existingHook);
251
+ // Replace handler on the existing hook
252
+ try {
253
+ existingHook.onCommitFiberRoot = ourCommitHandler;
254
+ }
255
+ catch (e) {
256
+ console.error("[Sailfish Fiber] Failed to replace handler:", e);
257
+ }
258
+ console.log("[Sailfish] React Fiber hook installed (chained with existing)");
259
+ }
260
+ else {
261
+ // Create our handler
262
+ ourCommitHandler = createCommitHandler(null);
263
+ // Install our hook
264
+ window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
265
+ supportsFiber: true,
266
+ renderers: new Map(),
267
+ inject(renderer) {
268
+ const id = this.renderers.size + 1;
269
+ this.renderers.set(id, renderer);
270
+ return id;
271
+ },
272
+ onCommitFiberRoot: ourCommitHandler,
273
+ };
274
+ console.log("[Sailfish] React Fiber hook installed (fresh)");
275
+ }
276
+ hookInstalled = true;
277
+ return true;
278
+ }
279
+ catch (error) {
280
+ console.warn("[Sailfish] Failed to install fiber hook:", error);
281
+ return false;
282
+ }
283
+ }
284
+ /**
285
+ * Check if the fiber hook is installed.
286
+ */
287
+ export function isFiberHookInstalled() {
288
+ return hookInstalled;
289
+ }
290
+ /**
291
+ * Force process the current React tree by walking the DOM.
292
+ * Call this after React has mounted to capture the initial tree.
293
+ */
294
+ export function processExistingTree() {
295
+ if (typeof window === "undefined")
296
+ return;
297
+ if (!IS_DEV)
298
+ return;
299
+ let processedCount = 0;
300
+ let totalElements = 0;
301
+ try {
302
+ // Walk all elements in the document
303
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null);
304
+ let node = walker.currentNode;
305
+ while (node) {
306
+ totalElements++;
307
+ if (node instanceof HTMLElement) {
308
+ if (processElement(node)) {
309
+ processedCount++;
310
+ }
311
+ }
312
+ node = walker.nextNode();
313
+ }
314
+ }
315
+ catch (error) {
316
+ console.warn("[Sailfish Fiber] Error processing existing tree:", error);
317
+ }
318
+ console.log(`[Sailfish Fiber] Processed existing tree: ${processedCount} elements annotated out of ${totalElements} elements`);
319
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * React Fiber types for component visibility tracking.
3
+ * These interfaces represent the minimal subset of React's internal Fiber structure
4
+ * needed for instrumentation without coupling to React's implementation details.
5
+ */
6
+ // React Fiber work tags (subset we care about)
7
+ export const FiberTag = {
8
+ FunctionComponent: 0,
9
+ ClassComponent: 1,
10
+ IndeterminateComponent: 2,
11
+ HostRoot: 3,
12
+ HostPortal: 4,
13
+ HostComponent: 5,
14
+ HostText: 6,
15
+ Fragment: 7,
16
+ Mode: 8,
17
+ ContextConsumer: 9,
18
+ ContextProvider: 10,
19
+ ForwardRef: 11,
20
+ Profiler: 12,
21
+ SuspenseComponent: 13,
22
+ MemoComponent: 14,
23
+ SimpleMemoComponent: 15,
24
+ LazyComponent: 16,
25
+ };
package/dist/index.js CHANGED
@@ -39,6 +39,8 @@ const INCLUDE = "include";
39
39
  const SAME_ORIGIN = "same-origin";
40
40
  const ALLOWED_HEADERS_HEADER = "access-control-allow-headers";
41
41
  const OPTIONS = "OPTIONS";
42
+ const AUTHORIZATION_HEADER_KEY_LOWER = "authorization";
43
+ const AUTHORIZATION_HEADER_KEY = "Authorization";
42
44
  const ActionType = {
43
45
  PROPAGATE: "propagate",
44
46
  IGNORE: "ignore",
@@ -54,6 +56,7 @@ export const DEFAULT_CAPTURE_SETTINGS = {
54
56
  recordSsn: false,
55
57
  recordDob: false,
56
58
  sampling: {},
59
+ enableFiberTracking: true,
57
60
  };
58
61
  export const DEFAULT_CONSOLE_RECORDING_SETTINGS = {
59
62
  level: ["info", "log", "warn", "error"],
@@ -83,6 +86,41 @@ export const DEFAULT_CONSOLE_RECORDING_SETTINGS = {
83
86
  // recordBody: true,
84
87
  // recordInitialRequests: false,
85
88
  // };
89
+ function maskAuthorizationHeader(requestHeaders) {
90
+ const authKey = requestHeaders[AUTHORIZATION_HEADER_KEY_LOWER]
91
+ ? AUTHORIZATION_HEADER_KEY_LOWER
92
+ : requestHeaders[AUTHORIZATION_HEADER_KEY]
93
+ ? AUTHORIZATION_HEADER_KEY
94
+ : null;
95
+ if (!authKey) {
96
+ return;
97
+ }
98
+ const authValue = requestHeaders[authKey];
99
+ const spaceIndex = authValue.indexOf(" ");
100
+ if (spaceIndex !== -1) {
101
+ const scheme = authValue.slice(0, spaceIndex + 1); // "Bearer "
102
+ const token = authValue.slice(spaceIndex + 1);
103
+ if (token.length > 8) {
104
+ requestHeaders[authKey] =
105
+ scheme +
106
+ token.slice(0, 4) +
107
+ "*".repeat(token.length - 8) +
108
+ token.slice(-4);
109
+ }
110
+ else {
111
+ requestHeaders[authKey] = scheme + "*".repeat(token.length);
112
+ }
113
+ }
114
+ else if (authValue.length > 8) {
115
+ requestHeaders[authKey] =
116
+ authValue.slice(0, 4) +
117
+ "*".repeat(authValue.length - 8) +
118
+ authValue.slice(-4);
119
+ }
120
+ else {
121
+ requestHeaders[authKey] = "*".repeat(authValue.length);
122
+ }
123
+ }
86
124
  function trackDomainChangesOnce() {
87
125
  const g = (window.__sailfish_recorder ||= {});
88
126
  if (g.routeWatcherIntervalId) {
@@ -403,12 +441,11 @@ function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo = []) {
403
441
  const startTime = Date.now();
404
442
  let finished = false;
405
443
  const requestBody = args[0]; // Capture the request body/payload
406
- const requestHeaders = { ...this._capturedRequestHeaders }; // Capture request headers
407
- // Filter out internal Sailfish headers from the captured headers
408
- delete requestHeaders[xSf3RidHeader];
409
- if (funcSpanHeader) {
410
- delete requestHeaders[funcSpanHeader.name];
411
- }
444
+ // Note: _capturedRequestHeaders already includes x-sf3-rid and funcSpan headers
445
+ // because setRequestHeader() above goes through our intercepted version
446
+ const requestHeaders = { ...this._capturedRequestHeaders };
447
+ // Mask authorization header for privacy (preserve scheme like "Bearer", mask the token)
448
+ maskAuthorizationHeader(requestHeaders);
412
449
  // 4️⃣ Helper to emit networkRequestFinished
413
450
  const emitFinished = (success, status, errorMsg, responseData, responseHeaders) => {
414
451
  if (finished)
@@ -576,20 +613,28 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
576
613
  console.warn("[Sailfish] Failed to capture request data:", e);
577
614
  }
578
615
  }
579
- // Filter out internal Sailfish headers from captured headers
616
+ // Remove any existing Sailfish headers (in case app already set them)
580
617
  delete requestHeaders[xSf3RidHeader];
581
- const funcSpanHeaderName = getFuncSpanHeader()?.name;
582
- if (funcSpanHeaderName) {
583
- delete requestHeaders[funcSpanHeaderName];
618
+ const funcSpanHeader = getFuncSpanHeader();
619
+ if (funcSpanHeader) {
620
+ delete requestHeaders[funcSpanHeader.name];
621
+ }
622
+ // Add Sailfish headers with correct values
623
+ const xSf3RidValue = `${sessionId}/${urlAndStoredUuids.page_visit_uuid}/${networkUUID}`;
624
+ requestHeaders[xSf3RidHeader] = xSf3RidValue;
625
+ if (funcSpanHeader) {
626
+ requestHeaders[funcSpanHeader.name] = funcSpanHeader.value;
584
627
  }
628
+ // Mask authorization header for privacy (preserve scheme like "Bearer", mask the token)
629
+ maskAuthorizationHeader(requestHeaders);
585
630
  try {
586
631
  let response = await injectHeader(target, thisArg, input, init, sessionId, urlAndStoredUuids.page_visit_uuid, networkUUID);
587
632
  let isRetry = false;
588
633
  // Retry logic for 400/403 before logging finished event
589
634
  if (BAD_HTTP_STATUS.includes(response.status)) {
590
635
  DEBUG && console.log("Perform retry as status was fail:", response);
591
- // Generate a NEW UUID for the retry request so each request has a unique ID
592
- networkUUID = uuidv4();
636
+ // Remove x-sf3-rid from captured headers since retry doesn't include it
637
+ delete requestHeaders[xSf3RidHeader];
593
638
  response = await retryWithoutPropagateHeaders(target, thisArg, args, url);
594
639
  isRetry = true;
595
640
  }