@sailfish-ai/recorder 1.10.5 → 1.10.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.
Binary file
Binary file
@@ -1,9 +1,12 @@
1
1
  import { STORAGE_KEYS } from "./types";
2
+ import { HAS_LOCAL_STORAGE } from "../runtimeEnv";
2
3
  // Helper function to load user preferences from localStorage
3
4
  function loadUserPreferences() {
4
5
  return {
5
- createIssue: localStorage.getItem(STORAGE_KEYS.CREATE_ISSUE) === "true",
6
- createEngTicket: localStorage.getItem(STORAGE_KEYS.CREATE_ENG_TICKET) === "true",
6
+ createIssue: HAS_LOCAL_STORAGE &&
7
+ localStorage.getItem(STORAGE_KEYS.CREATE_ISSUE) === "true",
8
+ createEngTicket: HAS_LOCAL_STORAGE &&
9
+ localStorage.getItem(STORAGE_KEYS.CREATE_ENG_TICKET) === "true",
7
10
  };
8
11
  }
9
12
  // Helper function to get initial state
package/dist/index.js CHANGED
@@ -8,11 +8,12 @@ import { readGitSha } from "./env";
8
8
  import { initializeErrorInterceptor } from "./errorInterceptor";
9
9
  import { fetchCaptureSettings, fetchFunctionSpanTrackingEnabled, sendDomainsToNotPropagateHeaderTo, startRecordingSession, } from "./graphql";
10
10
  import { sendMapUuidIfAvailable } from "./mapUuid";
11
- import { getUrlAndStoredUuids, initializeConsolePlugin, initializeDomContentEvents, initializeRecording, } from "./recording";
11
+ import { getUrlAndStoredUuids, initializeConsolePlugin, initializeDomContentEvents, initializeRecording, invalidateUrlCache, } from "./recording";
12
12
  import { HAS_DOCUMENT, HAS_LOCAL_STORAGE, HAS_SESSION_STORAGE, HAS_WINDOW, } from "./runtimeEnv";
13
13
  import { ensureSessionListeners, getOrSetSessionId } from "./session";
14
14
  import { withAppUrlMetadata } from "./utils";
15
15
  import { clearStaleFuncSpanState, getFuncSpanHeader, isFunctionSpanTrackingEnabled, restoreFuncSpanState, sendEvent, sendMessage, } from "./websocket";
16
+ import { yieldToMain } from "./scheduler";
16
17
  const DEBUG = readDebugFlag(); // A wrapper around fetch that suppresses connection refused errors
17
18
  // Regex cache for matchUrlWithWildcard() — avoids recompiling on every network request
18
19
  const _regexCache = new Map();
@@ -148,6 +149,7 @@ function trackDomainChangesOnce() {
148
149
  const prevPageVisitUUID = sessionStorage.getItem("pageVisitUUID");
149
150
  sessionStorage.setItem("pageVisitUUID", pageVisitUUID);
150
151
  sessionStorage.setItem("prevPageVisitUUID", prevPageVisitUUID);
152
+ invalidateUrlCache();
151
153
  const timestamp = Date.now();
152
154
  sendMessage({
153
155
  type: "routeChange",
@@ -242,6 +244,7 @@ function handleVisibilityChange() {
242
244
  // Store visibility state in sessionStorage
243
245
  sessionStorage.setItem("tabVisibilityChanged", timestamp.toString());
244
246
  sessionStorage.setItem("tabVisibilityState", visibilityState);
247
+ invalidateUrlCache();
245
248
  }
246
249
  function clearPageVisitDataFromSessionStorage() {
247
250
  if (!HAS_SESSION_STORAGE)
@@ -250,6 +253,7 @@ function clearPageVisitDataFromSessionStorage() {
250
253
  sessionStorage.removeItem("prevPageVisitUUID");
251
254
  sessionStorage.removeItem("tabVisibilityChanged");
252
255
  sessionStorage.removeItem("tabVisibilityState");
256
+ invalidateUrlCache();
253
257
  }
254
258
  // Visibility/beforeunload listeners are deferred — see _ensureModuleSideEffects()
255
259
  // One-time deferred side effects that were previously at module top-level.
@@ -1013,14 +1017,9 @@ function getMapUuidFromWindow() {
1013
1017
  // Note - we do NOT send serviceIdentifier because
1014
1018
  // it would be 1 serviceIdentifier per frontend user session,
1015
1019
  // which is very wasteful
1016
- 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, enableFiberTracking = false, deferRecordingStart = true, useWsWorker = true, }) {
1017
- // Initialize deferred module-level side effects (one-time)
1018
- _ensureModuleSideEffects();
1019
- const effectiveGitSha = gitSha ?? readGitSha();
1020
- const effectiveServiceIdentifier = serviceIdentifier ?? "";
1021
- const effectiveServiceVersion = serviceVersion ?? "";
1022
- const effectiveLibrary = "JS/TS"; // from Recorder.LIBRARY_CHOICES
1023
- const effectiveMapUuid = getMapUuidFromWindow();
1020
+ 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, enableFiberTracking = false, deferRecording, deferRecordingStart, chunkSnapshot, useWsWorker = true, }) {
1021
+ // deferRecording takes precedence; fall back to deprecated deferRecordingStart; default true
1022
+ const effectiveDeferRecording = deferRecording ?? deferRecordingStart ?? true;
1024
1023
  const sessionId = getOrSetSessionId();
1025
1024
  const g = (window.__sailfish_recorder ||= {});
1026
1025
  g.sessionId = sessionId;
@@ -1035,32 +1034,60 @@ export async function startRecording({ apiKey, backendApi = "https://api-service
1035
1034
  trackDomainChangesOnce(); // ensure single watcher alive
1036
1035
  return;
1037
1036
  }
1038
- // One-time installs (per window)
1037
+ // ── Phase 1: Critical interceptor installs ───────────────────────────────
1038
+ // These interceptors MUST be installed before app code makes network
1039
+ // requests or logs to the console. Yield between each setup to ensure
1040
+ // no single task exceeds 50ms (especially on mobile with CPU throttle).
1041
+ const bodyCaptureConfig = {
1042
+ captureStreamingResponseBody,
1043
+ captureResponseBodyMaxMb,
1044
+ captureStreamPrefixKb,
1045
+ captureStreamTimeoutMs,
1046
+ };
1047
+ // 1a. XHR interceptor
1048
+ if (!g.xhrPatched) {
1049
+ setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo, bodyCaptureConfig);
1050
+ g.xhrPatched = true;
1051
+ }
1052
+ await yieldToMain();
1053
+ // 1b. Fetch interceptor
1054
+ if (!g.fetchPatched) {
1055
+ setupFetchInterceptor(domainsToNotPropagateHeaderTo, bodyCaptureConfig);
1056
+ g.fetchPatched = true;
1057
+ }
1058
+ await yieldToMain();
1059
+ // 1c. DOM content events
1039
1060
  if (!g.domEventsInit) {
1040
1061
  initializeDomContentEvents(sessionId);
1041
1062
  g.domEventsInit = true;
1042
1063
  }
1064
+ await yieldToMain();
1065
+ // 1d. Console plugin (fire-and-forget dynamic import)
1043
1066
  if (!g.consoleInit) {
1044
1067
  initializeConsolePlugin(DEFAULT_CONSOLE_RECORDING_SETTINGS, sessionId);
1045
1068
  g.consoleInit = true;
1046
1069
  }
1070
+ await yieldToMain();
1071
+ // 1e. Error interceptor
1047
1072
  if (!g.errorInit) {
1048
1073
  initializeErrorInterceptor();
1049
1074
  g.errorInit = true;
1050
1075
  }
1076
+ await yieldToMain();
1077
+ // ── Phase 2: Deferred async work ────────────────────────────────────────
1078
+ // Everything else — GraphQL calls, WebSocket init, device info, domain
1079
+ // reporting, visibility listeners, funcspan state restore, etc.
1080
+ // None of this needs to happen before app code runs.
1081
+ // Deferred module-level side effects (funcspan restore, visibility listeners, etc.)
1082
+ _ensureModuleSideEffects();
1051
1083
  storeCredentialsAndConnection({ apiKey, backendApi });
1052
- trackDomainChangesOnce();
1053
- sessionStorage.setItem("sailfishApiKey", apiKey);
1054
- sessionStorage.setItem("sailfishBackendApi", backendApi);
1055
- // Function span tracking state is loaded from localStorage on module init (websocket.tsx)
1056
- // Validate localStorage state with backend if tracking appears to be enabled but WebSocket not connected
1084
+ // Function span tracking state validation (fire-and-forget)
1057
1085
  if (isFunctionSpanTrackingEnabled() && (!g.ws || g.ws.readyState !== 1)) {
1058
1086
  fetchFunctionSpanTrackingEnabled(apiKey, backendApi)
1059
1087
  .then((funcSpanResponse) => {
1060
1088
  const isEnabled = funcSpanResponse.data?.isFunctionSpanTrackingEnabledFromApiKey ??
1061
1089
  false;
1062
1090
  if (!isEnabled) {
1063
- // Backend says tracking is NOT active, clear stale localStorage data
1064
1091
  clearStaleFuncSpanState();
1065
1092
  if (DEBUG) {
1066
1093
  console.log("[Sailfish] Cleared stale function span tracking state - backend validation shows tracking is not active");
@@ -1076,7 +1103,6 @@ export async function startRecording({ apiKey, backendApi = "https://api-service
1076
1103
  if (DEBUG) {
1077
1104
  console.warn("[Sailfish] Failed to validate function span tracking status with backend:", error);
1078
1105
  }
1079
- // On error, keep the localStorage state and let WebSocket correct it when it connects
1080
1106
  });
1081
1107
  }
1082
1108
  if (!g.sentDoNotPropagateOnce) {
@@ -1086,28 +1112,19 @@ export async function startRecording({ apiKey, backendApi = "https://api-service
1086
1112
  ], backendApi).catch((error) => console.error("Failed to send domains to not propagate header to:", error));
1087
1113
  g.sentDoNotPropagateOnce = true;
1088
1114
  }
1089
- // Network body capture config
1090
- const bodyCaptureConfig = {
1091
- captureStreamingResponseBody,
1092
- captureResponseBodyMaxMb,
1093
- captureStreamPrefixKb,
1094
- captureStreamTimeoutMs,
1095
- };
1096
- // Patch XHR/fetch once per window
1097
- if (!g.xhrPatched) {
1098
- setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo, bodyCaptureConfig);
1099
- g.xhrPatched = true;
1100
- }
1101
- if (!g.fetchPatched) {
1102
- setupFetchInterceptor(domainsToNotPropagateHeaderTo, bodyCaptureConfig);
1103
- g.fetchPatched = true;
1104
- }
1105
1115
  gatherAndCacheDeviceInfo();
1106
1116
  if (enableIpTracking) {
1107
1117
  fetchAndSendIp(sessionId);
1108
1118
  }
1109
1119
  try {
1120
+ const effectiveGitSha = gitSha ?? readGitSha();
1121
+ const effectiveServiceIdentifier = serviceIdentifier ?? "";
1122
+ const effectiveServiceVersion = serviceVersion ?? "";
1123
+ const effectiveLibrary = "JS/TS";
1124
+ const effectiveMapUuid = getMapUuidFromWindow();
1110
1125
  const metadataWithAppUrl = withAppUrlMetadata(serviceAdditionalMetadata);
1126
+ // Yield before GraphQL calls
1127
+ await yieldToMain();
1111
1128
  // Fire both GraphQL calls in parallel — they are independent
1112
1129
  const [captureSettingsResponse, sessionResponse] = await Promise.all([
1113
1130
  fetchCaptureSettings(apiKey, backendApi),
@@ -1123,12 +1140,15 @@ export async function startRecording({ apiKey, backendApi = "https://api-service
1123
1140
  return;
1124
1141
  }
1125
1142
  if (sessionResponse.data?.startRecordingSession) {
1126
- // Extract env value from serviceAdditionalMetadata
1127
1143
  const envValue = serviceAdditionalMetadata?.env ||
1128
1144
  serviceAdditionalMetadata?.environment;
1129
- const websocket = await initializeRecording(captureSettings, backendApi, apiKey, sessionId, envValue, deferRecordingStart, useWsWorker);
1145
+ // Yield before WebSocket initialization
1146
+ await yieldToMain();
1147
+ const websocket = await initializeRecording(captureSettings, backendApi, apiKey, sessionId, envValue, effectiveDeferRecording, useWsWorker, chunkSnapshot ?? false);
1130
1148
  g.ws = websocket;
1131
1149
  g.initialized = true;
1150
+ // Route watcher and mapUuid run after WS is connected
1151
+ trackDomainChangesOnce();
1132
1152
  if (!g.sentMapUuidOnce) {
1133
1153
  sendMapUuidIfAvailable(serviceIdentifier, serviceVersion);
1134
1154
  g.sentMapUuidOnce = true;