@sailfish-ai/recorder 1.10.2 → 1.10.4

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.
Files changed (38) hide show
  1. package/dist/chunks/fiberHook-CEzmPkx_.js +125 -0
  2. package/dist/chunks/fiberHook-CEzmPkx_.js.br +0 -0
  3. package/dist/chunks/fiberHook-CEzmPkx_.js.gz +0 -0
  4. package/dist/chunks/fiberHook-DGANQ2ma.js +130 -0
  5. package/dist/chunks/fiberHook-DGANQ2ma.js.br +0 -0
  6. package/dist/chunks/fiberHook-DGANQ2ma.js.gz +0 -0
  7. package/dist/chunks/rrweb-plugin-console-record-BmAm-Ih_.js +181 -0
  8. package/dist/chunks/rrweb-plugin-console-record-BmAm-Ih_.js.br +0 -0
  9. package/dist/chunks/rrweb-plugin-console-record-BmAm-Ih_.js.gz +0 -0
  10. package/dist/chunks/rrweb-plugin-console-record-Cr-osXuj.js +180 -0
  11. package/dist/chunks/rrweb-plugin-console-record-Cr-osXuj.js.br +0 -0
  12. package/dist/chunks/rrweb-plugin-console-record-Cr-osXuj.js.gz +0 -0
  13. package/dist/chunks/rrweb-record-only-Ba4xyfd6.js +5253 -0
  14. package/dist/chunks/rrweb-record-only-Ba4xyfd6.js.br +0 -0
  15. package/dist/chunks/rrweb-record-only-Ba4xyfd6.js.gz +0 -0
  16. package/dist/chunks/rrweb-record-only-C5Qb-uaQ.js +5253 -0
  17. package/dist/chunks/rrweb-record-only-C5Qb-uaQ.js.br +0 -0
  18. package/dist/chunks/rrweb-record-only-C5Qb-uaQ.js.gz +0 -0
  19. package/dist/inAppReportIssueModal/index.js +171 -129
  20. package/dist/inAppReportIssueModal/integrations.js +84 -19
  21. package/dist/inAppReportIssueModal/state.js +1 -0
  22. package/dist/inAppReportIssueModal/types.js +1 -0
  23. package/dist/inAppReportIssueModal/ui.js +9 -0
  24. package/dist/index.js +259 -60
  25. package/dist/recorder.cjs +1954 -7344
  26. package/dist/recorder.js +1953 -7344
  27. package/dist/recorder.js.br +0 -0
  28. package/dist/recorder.js.gz +0 -0
  29. package/dist/recording.js +41 -32
  30. package/dist/session.js +12 -6
  31. package/dist/types/inAppReportIssueModal/integrations.d.ts +8 -0
  32. package/dist/types/inAppReportIssueModal/types.d.ts +3 -4
  33. package/dist/types/index.d.ts +11 -3
  34. package/dist/types/recording.d.ts +2 -2
  35. package/dist/types/session.d.ts +1 -0
  36. package/dist/types/websocket.d.ts +1 -0
  37. package/dist/websocket.js +11 -10
  38. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7,15 +7,26 @@ import { fetchAndSendIp } from "./sendSailfishMessages";
7
7
  import { readGitSha } from "./env";
8
8
  import { initializeErrorInterceptor } from "./errorInterceptor";
9
9
  import { fetchCaptureSettings, fetchFunctionSpanTrackingEnabled, sendDomainsToNotPropagateHeaderTo, startRecordingSession, } from "./graphql";
10
- import { setupIssueReporting } from "./inAppReportIssueModal";
11
- import { fetchIntegrationData, getIntegrationData, } from "./inAppReportIssueModal/integrations";
12
10
  import { sendMapUuidIfAvailable } from "./mapUuid";
13
11
  import { getUrlAndStoredUuids, initializeConsolePlugin, initializeDomContentEvents, initializeRecording, } from "./recording";
14
12
  import { HAS_DOCUMENT, HAS_LOCAL_STORAGE, HAS_SESSION_STORAGE, HAS_WINDOW, } from "./runtimeEnv";
15
- import { getOrSetSessionId } from "./session";
13
+ import { ensureSessionListeners, getOrSetSessionId } from "./session";
16
14
  import { withAppUrlMetadata } from "./utils";
17
- import { clearStaleFuncSpanState, getFuncSpanHeader, isFunctionSpanTrackingEnabled, sendEvent, sendMessage, } from "./websocket";
15
+ import { clearStaleFuncSpanState, getFuncSpanHeader, isFunctionSpanTrackingEnabled, restoreFuncSpanState, sendEvent, sendMessage, } from "./websocket";
18
16
  const DEBUG = readDebugFlag(); // A wrapper around fetch that suppresses connection refused errors
17
+ // Regex cache for matchUrlWithWildcard() — avoids recompiling on every network request
18
+ const _regexCache = new Map();
19
+ function getCachedRegex(pattern, flags) {
20
+ const key = flags ? `${pattern}|${flags}` : pattern;
21
+ let re = _regexCache.get(key);
22
+ if (!re) {
23
+ re = new RegExp(pattern, flags);
24
+ _regexCache.set(key, re);
25
+ }
26
+ return re;
27
+ }
28
+ // Pre-built Set for O(1) static extension lookups (replaces linear STATIC_EXTENSIONS array scan)
29
+ const STATIC_EXTENSIONS_SET = new Set(STATIC_EXTENSIONS.map(ext => ext.toLowerCase()));
19
30
  // Default list of domains to ignore
20
31
  const DOMAINS_TO_NOT_PROPAGATE_HEADER_TO_DEFAULT = [
21
32
  "t.co",
@@ -57,7 +68,7 @@ export const DEFAULT_CAPTURE_SETTINGS = {
57
68
  recordSsn: false,
58
69
  recordDob: false,
59
70
  sampling: {},
60
- enableFiberTracking: true,
71
+ enableFiberTracking: false,
61
72
  };
62
73
  export const DEFAULT_CONSOLE_RECORDING_SETTINGS = {
63
74
  level: ["info", "log", "warn", "error"],
@@ -180,11 +191,7 @@ function sendTimeZone() {
180
191
  };
181
192
  sendMessage(message);
182
193
  }
183
- // Send standard information like userDeviceUuid and timeZone
184
- if (HAS_WINDOW) {
185
- sendUserDeviceUuid();
186
- sendTimeZone();
187
- }
194
+ // Side effects deferred into startRecording() see _ensureModuleSideEffects()
188
195
  // Function to get or set the device & program UUID in localStorage
189
196
  function getOrSetUserDeviceUuid() {
190
197
  let userDeviceUuid = null;
@@ -244,15 +251,32 @@ function clearPageVisitDataFromSessionStorage() {
244
251
  sessionStorage.removeItem("tabVisibilityChanged");
245
252
  sessionStorage.removeItem("tabVisibilityState");
246
253
  }
247
- // Initialize event listeners for visibility change and page unload
248
- if (HAS_DOCUMENT) {
249
- document.addEventListener("visibilitychange", handleVisibilityChange);
250
- }
251
- if (HAS_WINDOW) {
252
- window.addEventListener("beforeunload", () => {
253
- // window.name = "";
254
- clearPageVisitDataFromSessionStorage();
255
- });
254
+ // Visibility/beforeunload listeners are deferred see _ensureModuleSideEffects()
255
+ // One-time deferred side effects that were previously at module top-level.
256
+ // Called once at the start of startRecording() to avoid import-time work.
257
+ let _moduleSideEffectsInit = false;
258
+ function _ensureModuleSideEffects() {
259
+ if (_moduleSideEffectsInit)
260
+ return;
261
+ _moduleSideEffectsInit = true;
262
+ // Restore funcspan state from localStorage (was an IIFE in websocket.tsx)
263
+ restoreFuncSpanState();
264
+ // Session beforeunload listener (was module-level in session.tsx)
265
+ ensureSessionListeners();
266
+ // Send standard information
267
+ if (HAS_WINDOW) {
268
+ sendUserDeviceUuid();
269
+ sendTimeZone();
270
+ }
271
+ // Visibility change and page unload listeners
272
+ if (HAS_DOCUMENT) {
273
+ document.addEventListener("visibilitychange", handleVisibilityChange);
274
+ }
275
+ if (HAS_WINDOW) {
276
+ window.addEventListener("beforeunload", () => {
277
+ clearPageVisitDataFromSessionStorage();
278
+ });
279
+ }
256
280
  }
257
281
  function storeCredentialsAndConnection({ apiKey, backendApi, }) {
258
282
  if (!HAS_SESSION_STORAGE)
@@ -311,8 +335,8 @@ export function matchUrlWithWildcard(input, patterns) {
311
335
  const normalizedPatternDomain = patternDomain
312
336
  .replace(/\./g, "\\.") // Escape dots for regex
313
337
  .replace(/\*/g, ".*"); // Replace '*' with regex to match any characters
314
- // Create regex for the domain pattern
315
- const domainRegex = new RegExp(`^${normalizedPatternDomain}$`, "i");
338
+ // Create regex for the domain pattern (cached)
339
+ const domainRegex = getCachedRegex(`^${normalizedPatternDomain}$`, "i");
316
340
  // Strip 'www.' from both the input domain and the pattern domain for comparison
317
341
  const strippedDomain = domain.startsWith("www.") ? domain.slice(4) : domain;
318
342
  // If pattern specifies a port, match the exact port
@@ -330,7 +354,7 @@ export function matchUrlWithWildcard(input, patterns) {
330
354
  const normalizedPatternPath = patternPath
331
355
  .replace(/\*/g, ".*") // Replace '*' with regex to match any characters
332
356
  .replace(/\/$/, ""); // Remove trailing slashes from pattern
333
- const pathRegex = new RegExp(`^/${normalizedPatternPath}`, "i");
357
+ const pathRegex = getCachedRegex(`^/${normalizedPatternPath}`, "i");
334
358
  return pathRegex.test(pathname); // Match the path
335
359
  }
336
360
  return true; // Domain matched, no path required
@@ -343,7 +367,7 @@ export function matchUrlWithWildcard(input, patterns) {
343
367
  const normalizedPatternPath = patternPath
344
368
  .replace(/\*/g, ".*") // Replace '*' with regex to match any characters
345
369
  .replace(/\/$/, ""); // Remove trailing slashes from pattern
346
- const pathRegex = new RegExp(`^/${normalizedPatternPath}`, "i");
370
+ const pathRegex = getCachedRegex(`^/${normalizedPatternPath}`, "i");
347
371
  return pathRegex.test(pathname); // Match the path
348
372
  }
349
373
  // If no path pattern, only the domain needs to match
@@ -359,11 +383,11 @@ function shouldSkipHeadersPropagation(url, domainsToNotPropagateHeaderTo = []) {
359
383
  // If we cannot parse, play it safe and do NOT inject headers.
360
384
  return true;
361
385
  }
362
- // 1️⃣ STATIC ASSET EXCLUSIONS (by comprehensive file extension list)
363
- for (const ext of STATIC_EXTENSIONS) {
364
- if (urlObj.pathname.toLowerCase().endsWith(ext)) {
365
- return true;
366
- }
386
+ // 1️⃣ STATIC ASSET EXCLUSIONS (by comprehensive file extension list) — O(1) Set lookup
387
+ const lowerPathname = urlObj.pathname.toLowerCase();
388
+ const lastDotIdx = lowerPathname.lastIndexOf(".");
389
+ if (lastDotIdx !== -1 && STATIC_EXTENSIONS_SET.has(lowerPathname.slice(lastDotIdx))) {
390
+ return true;
367
391
  }
368
392
  // 2️⃣ WILDCARD-BASED EXCLUSION (domain + path)
369
393
  // Pass patterns like ["*.cdn.com/*", "api.example.com/v1/*"]
@@ -377,7 +401,7 @@ function shouldSkipHeadersPropagation(url, domainsToNotPropagateHeaderTo = []) {
377
401
  return false;
378
402
  }
379
403
  // Updated XMLHttpRequest interceptor with domain exclusion
380
- function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo = []) {
404
+ function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo = [], bodyCaptureConfig = { captureStreamingResponseBody: true, captureResponseBodyMaxMb: 10, captureStreamPrefixKb: 64, captureStreamTimeoutMs: 10000 }) {
381
405
  const originalOpen = XMLHttpRequest.prototype.open;
382
406
  const originalSend = XMLHttpRequest.prototype.send;
383
407
  const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
@@ -480,9 +504,23 @@ function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo = []) {
480
504
  const status = this.status || 0;
481
505
  let responseData;
482
506
  let responseHeaders = null;
507
+ const maxBodyBytes = bodyCaptureConfig.captureResponseBodyMaxMb * 1024 * 1024;
483
508
  try {
484
- // Try to capture response data
485
- responseData = this.responseText || this.response;
509
+ if (bodyCaptureConfig.captureResponseBodyMaxMb === 0) {
510
+ // Body capture disabled
511
+ responseData = null;
512
+ }
513
+ else {
514
+ // Try to capture response data
515
+ const rawData = this.responseText || this.response;
516
+ if (typeof rawData === 'string' && rawData.length > maxBodyBytes) {
517
+ // Response exceeds size limit — skip
518
+ responseData = null;
519
+ }
520
+ else {
521
+ responseData = rawData;
522
+ }
523
+ }
486
524
  }
487
525
  catch (e) {
488
526
  // Response might not be accessible in some cases
@@ -530,9 +568,93 @@ function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo = []) {
530
568
  };
531
569
  }
532
570
  // Updated fetch interceptor with exclusion handling
533
- function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
571
+ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = [], bodyCaptureConfig = { captureStreamingResponseBody: true, captureResponseBodyMaxMb: 10, captureStreamPrefixKb: 64, captureStreamTimeoutMs: 10000 }) {
534
572
  const originalFetch = window.fetch;
535
573
  const sessionId = getOrSetSessionId();
574
+ // --- Streaming detection helpers ---
575
+ // Content types that indicate a streaming response.
576
+ // These responses should NOT have their body fully consumed — only a limited prefix is captured.
577
+ const STREAMING_CONTENT_TYPES = [
578
+ 'text/event-stream', // Server-Sent Events (SSE)
579
+ 'application/x-ndjson', // Newline-delimited JSON
580
+ 'application/stream+json', // Spring WebFlux / reactive streams
581
+ 'application/grpc', // gRPC-Web
582
+ 'application/grpc-web', // gRPC-Web alternative
583
+ ];
584
+ // Content types where body capture should be skipped entirely (binary data as text is useless).
585
+ const SKIP_BODY_CONTENT_TYPES = [
586
+ 'application/octet-stream',
587
+ ];
588
+ function isStreamingResponse(response) {
589
+ const contentType = response.headers.get('content-type');
590
+ if (!contentType)
591
+ return false;
592
+ const lowerCT = contentType.toLowerCase();
593
+ return STREAMING_CONTENT_TYPES.some(t => lowerCT.includes(t));
594
+ }
595
+ function shouldSkipBodyCapture(response) {
596
+ const contentType = response.headers.get('content-type');
597
+ if (!contentType)
598
+ return false;
599
+ const lowerCT = contentType.toLowerCase();
600
+ return SKIP_BODY_CONTENT_TYPES.some(t => lowerCT.includes(t));
601
+ }
602
+ /**
603
+ * Reads a limited prefix from a cloned response's ReadableStream body.
604
+ * Respects a byte limit and timeout. Cancels the reader when done to free resources.
605
+ * Returns the captured text or null on error.
606
+ */
607
+ async function readStreamPrefix(clonedResponse, maxBytes, timeoutMs) {
608
+ const body = clonedResponse.body;
609
+ if (!body)
610
+ return null;
611
+ const reader = body.getReader();
612
+ const decoder = new TextDecoder();
613
+ const chunks = [];
614
+ let totalBytes = 0;
615
+ const readWithLimit = async () => {
616
+ try {
617
+ while (totalBytes < maxBytes) {
618
+ const { done, value } = await reader.read();
619
+ if (done)
620
+ break;
621
+ totalBytes += value.byteLength;
622
+ chunks.push(decoder.decode(value, { stream: true }));
623
+ }
624
+ // Flush the decoder
625
+ chunks.push(decoder.decode());
626
+ return chunks.join('');
627
+ }
628
+ catch {
629
+ return chunks.length > 0 ? chunks.join('') : null;
630
+ }
631
+ finally {
632
+ try {
633
+ reader.cancel();
634
+ }
635
+ catch { /* ignore */ }
636
+ }
637
+ };
638
+ try {
639
+ return await Promise.race([
640
+ readWithLimit(),
641
+ new Promise((resolve) => setTimeout(() => {
642
+ try {
643
+ reader.cancel();
644
+ }
645
+ catch { /* ignore */ }
646
+ resolve(chunks.length > 0 ? chunks.join('') : null);
647
+ }, timeoutMs)),
648
+ ]);
649
+ }
650
+ catch {
651
+ try {
652
+ reader.cancel();
653
+ }
654
+ catch { /* ignore */ }
655
+ return null;
656
+ }
657
+ }
536
658
  window.fetch = new Proxy(originalFetch, {
537
659
  apply: async (target, thisArg, args) => {
538
660
  let input = args[0];
@@ -573,19 +695,21 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
573
695
  // Capture request headers and body
574
696
  let requestHeaders = {};
575
697
  let requestBody;
698
+ // For Request inputs, we clone BEFORE fetch (since fetch consumes the body)
699
+ // but defer .text() read to avoid blocking
700
+ let requestBodyClone = null;
576
701
  try {
577
702
  if (input instanceof Request) {
578
703
  // Extract headers from Request object
579
704
  input.headers.forEach((value, key) => {
580
705
  requestHeaders[key] = value;
581
706
  });
582
- // Try to clone and read body if present
707
+ // Clone the Request NOW (before fetch consumes it) for deferred body reading
583
708
  try {
584
- const clonedRequest = input.clone();
585
- requestBody = await clonedRequest.text();
709
+ requestBodyClone = input.clone();
586
710
  }
587
711
  catch (e) {
588
- requestBody = null;
712
+ requestBodyClone = null;
589
713
  }
590
714
  }
591
715
  else {
@@ -605,7 +729,7 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
605
729
  requestHeaders = { ...init.headers };
606
730
  }
607
731
  }
608
- // Capture request body
732
+ // Capture request body (non-blocking for string/object bodies)
609
733
  requestBody = init.body;
610
734
  }
611
735
  }
@@ -643,20 +767,7 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
643
767
  const status = response.status;
644
768
  const success = response.ok;
645
769
  const error = success ? "" : `Request Error: ${response.statusText}`;
646
- // Capture response data
647
- let responseData;
648
- try {
649
- // Clone the response so we don't consume the original stream
650
- const clonedResponse = response.clone();
651
- responseData = await clonedResponse.text();
652
- }
653
- catch (e) {
654
- if (DEBUG) {
655
- console.warn("[Sailfish] Failed to capture response data:", e);
656
- }
657
- responseData = null;
658
- }
659
- // Capture response headers
770
+ // Capture response headers (non-blocking — just reads header map)
660
771
  let responseHeaders = null;
661
772
  try {
662
773
  responseHeaders = {};
@@ -670,8 +781,8 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
670
781
  }
671
782
  responseHeaders = null;
672
783
  }
673
- // Emit 'networkRequestFinished' event
674
- const eventData = {
784
+ // Build the base event data (response_body filled asynchronously below)
785
+ const baseEventData = {
675
786
  type: NetworkRequestEventId,
676
787
  timestamp: endTime,
677
788
  sessionId,
@@ -689,11 +800,75 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
689
800
  request_headers: requestHeaders,
690
801
  request_body: requestBody,
691
802
  response_headers: responseHeaders,
692
- response_body: responseData,
803
+ response_body: null,
693
804
  },
694
805
  ...urlAndStoredUuids,
695
806
  };
696
- sendEvent(eventData);
807
+ // Helper to resolve deferred request body and send event
808
+ const sendEventWithBody = (responseData) => {
809
+ baseEventData.data.response_body = responseData;
810
+ if (requestBodyClone) {
811
+ requestBodyClone.text().then((body) => { baseEventData.data.request_body = body; sendEvent(baseEventData); }, () => { sendEvent(baseEventData); });
812
+ }
813
+ else {
814
+ sendEvent(baseEventData);
815
+ }
816
+ };
817
+ const maxBodyBytes = bodyCaptureConfig.captureResponseBodyMaxMb * 1024 * 1024;
818
+ // --- Determine body capture strategy and return response IMMEDIATELY ---
819
+ if (bodyCaptureConfig.captureResponseBodyMaxMb === 0) {
820
+ // All body capture disabled
821
+ sendEventWithBody(null);
822
+ }
823
+ else if (shouldSkipBodyCapture(response)) {
824
+ // Binary content type — text capture is useless
825
+ sendEventWithBody(null);
826
+ }
827
+ else if (isStreamingResponse(response)) {
828
+ // Streaming response
829
+ if (bodyCaptureConfig.captureStreamingResponseBody) {
830
+ // Tee the stream: clone, capture limited prefix in background, cancel reader when done
831
+ try {
832
+ const clonedResponse = response.clone();
833
+ readStreamPrefix(clonedResponse, bodyCaptureConfig.captureStreamPrefixKb * 1024, bodyCaptureConfig.captureStreamTimeoutMs).then((prefix) => sendEventWithBody(prefix), () => sendEventWithBody(null));
834
+ }
835
+ catch {
836
+ sendEventWithBody(null);
837
+ }
838
+ }
839
+ else {
840
+ // Streaming body capture disabled — don't clone, send immediately
841
+ sendEventWithBody(null);
842
+ }
843
+ }
844
+ else {
845
+ // Non-streaming response — check Content-Length against size limit
846
+ const contentLength = response.headers.get('content-length');
847
+ const declaredSize = contentLength ? parseInt(contentLength, 10) : NaN;
848
+ if (!isNaN(declaredSize) && declaredSize > maxBodyBytes) {
849
+ // Response too large — skip body capture
850
+ sendEventWithBody(null);
851
+ }
852
+ else {
853
+ // Clone and read body asynchronously (fire-and-forget)
854
+ try {
855
+ const clonedResponse = response.clone();
856
+ clonedResponse.text().then((text) => {
857
+ // Double-check actual size after reading (Content-Length may be absent/wrong)
858
+ if (text.length > maxBodyBytes) {
859
+ sendEventWithBody(null);
860
+ }
861
+ else {
862
+ sendEventWithBody(text);
863
+ }
864
+ }, () => sendEventWithBody(null));
865
+ }
866
+ catch {
867
+ sendEventWithBody(null);
868
+ }
869
+ }
870
+ }
871
+ // CRITICAL: Return response to caller IMMEDIATELY — never block on body capture
697
872
  return response;
698
873
  }
699
874
  catch (error) {
@@ -706,6 +881,16 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
706
881
  // CORS/network failure: exclude just this path
707
882
  return target.apply(thisArg, args);
708
883
  }
884
+ // Resolve deferred request body for error telemetry
885
+ let resolvedRequestBody = requestBody;
886
+ if (requestBodyClone) {
887
+ try {
888
+ resolvedRequestBody = await requestBodyClone.text();
889
+ }
890
+ catch {
891
+ resolvedRequestBody = null;
892
+ }
893
+ }
709
894
  const eventData = {
710
895
  type: NetworkRequestEventId,
711
896
  timestamp: endTime,
@@ -721,7 +906,7 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
721
906
  method,
722
907
  url,
723
908
  request_headers: requestHeaders,
724
- request_body: requestBody,
909
+ request_body: resolvedRequestBody,
725
910
  response_body: null,
726
911
  },
727
912
  ...urlAndStoredUuids,
@@ -824,7 +1009,9 @@ function getMapUuidFromWindow() {
824
1009
  // Note - we do NOT send serviceIdentifier because
825
1010
  // it would be 1 serviceIdentifier per frontend user session,
826
1011
  // which is very wasteful
827
- export async function startRecording({ apiKey, backendApi = "https://api-service.sailfishqa.com", domainsToPropagateHeaderTo = [], domainsToNotPropagateHeaderTo = [], serviceVersion, serviceIdentifier, gitSha, serviceAdditionalMetadata, enableIpTracking, }) {
1012
+ export async function startRecording({ apiKey, backendApi = "https://api-service.sailfishqa.com", domainsToPropagateHeaderTo = [], domainsToNotPropagateHeaderTo = [], serviceVersion, serviceIdentifier, gitSha, serviceAdditionalMetadata, enableIpTracking, captureStreamingResponseBody = true, captureResponseBodyMaxMb = 10, captureStreamPrefixKb = 64, captureStreamTimeoutMs = 10000, }) {
1013
+ // Initialize deferred module-level side effects (one-time)
1014
+ _ensureModuleSideEffects();
828
1015
  const effectiveGitSha = gitSha ?? readGitSha();
829
1016
  const effectiveServiceIdentifier = serviceIdentifier ?? "";
830
1017
  const effectiveServiceVersion = serviceVersion ?? "";
@@ -895,13 +1082,20 @@ export async function startRecording({ apiKey, backendApi = "https://api-service
895
1082
  ], backendApi).catch((error) => console.error("Failed to send domains to not propagate header to:", error));
896
1083
  g.sentDoNotPropagateOnce = true;
897
1084
  }
1085
+ // Network body capture config
1086
+ const bodyCaptureConfig = {
1087
+ captureStreamingResponseBody,
1088
+ captureResponseBodyMaxMb,
1089
+ captureStreamPrefixKb,
1090
+ captureStreamTimeoutMs,
1091
+ };
898
1092
  // Patch XHR/fetch once per window
899
1093
  if (!g.xhrPatched) {
900
- setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo);
1094
+ setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo, bodyCaptureConfig);
901
1095
  g.xhrPatched = true;
902
1096
  }
903
1097
  if (!g.fetchPatched) {
904
- setupFetchInterceptor(domainsToNotPropagateHeaderTo);
1098
+ setupFetchInterceptor(domainsToNotPropagateHeaderTo, bodyCaptureConfig);
905
1099
  g.fetchPatched = true;
906
1100
  }
907
1101
  gatherAndCacheDeviceInfo();
@@ -967,9 +1161,14 @@ export const initRecorder = async (options) => {
967
1161
  g.hasLoggedInitOnce = true;
968
1162
  }
969
1163
  await startRecording(options);
970
- // Set up the issue reporting UI once
1164
+ // Set up the issue reporting UI once (lazy-loaded)
971
1165
  if (!g.issueReportingInit) {
972
1166
  const backendApiUrl = options.backendApi ?? "https://api-service.sailfishqa.com";
1167
+ // Dynamically import issue reporting modules to reduce initial bundle
1168
+ const [{ setupIssueReporting }, { fetchIntegrationData, getIntegrationData }] = await Promise.all([
1169
+ import("./inAppReportIssueModal"),
1170
+ import("./inAppReportIssueModal/integrations"),
1171
+ ]);
973
1172
  // Fetch integration data before setting up issue reporting
974
1173
  let integrationData = null;
975
1174
  try {