@sailfish-ai/recorder 1.8.1 → 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/dist/recording.js CHANGED
@@ -85,6 +85,8 @@ export const getUrlAndStoredUuids = () => ({
85
85
  page_visit_uuid: sessionStorage.getItem("pageVisitUUID"),
86
86
  prev_page_visit_uuid: sessionStorage.getItem("prevPageVisitUUID"),
87
87
  href: location.origin + location.pathname,
88
+ tabVisibilityChanged: sessionStorage.getItem("tabVisibilityChanged"),
89
+ tabVisibilityState: sessionStorage.getItem("tabVisibilityState"),
88
90
  });
89
91
  export function initializeDomContentEvents(sessionId) {
90
92
  document.addEventListener("readystatechange", () => {
@@ -1,8 +1,3 @@
1
- /**
2
- * Resolves stack traces using source maps.
3
- * @param stackTrace - The minified stack trace.
4
- * @returns The mapped stack trace with original file/line/column.
5
- */
6
1
  export declare function resolveStackTrace(stackTrace?: string | string[]): Promise<string[]>;
7
2
  /**
8
3
  * Initializes the error interceptor globally.
@@ -4,6 +4,8 @@ export declare const getUrlAndStoredUuids: () => {
4
4
  page_visit_uuid: string;
5
5
  prev_page_visit_uuid: string;
6
6
  href: string;
7
+ tabVisibilityChanged: string;
8
+ tabVisibilityState: string;
7
9
  };
8
10
  export declare function initializeDomContentEvents(sessionId: string): void;
9
11
  export declare function initializeConsolePlugin(consoleRecordSettings: LogRecordOptions, sessionId: string): void;
@@ -3,3 +3,26 @@ export declare function flushBufferedEvents(): Promise<void>;
3
3
  export declare function sendEvent(event: any): void;
4
4
  export declare function initializeWebSocket(backendApi: string, apiKey: string, sessionId: string): ReconnectingWebSocket;
5
5
  export declare function sendMessage(message: Record<string, any>): void;
6
+ /**
7
+ * Enable function span tracking for this session (e.g., when report issue recording starts)
8
+ * This is LOCAL tracking mode - only this session, not global
9
+ */
10
+ export declare function enableFunctionSpanTracking(): void;
11
+ /**
12
+ * Disable function span tracking for this session (e.g., when report issue recording stops)
13
+ * Only disables if we're in LOCAL tracking mode
14
+ */
15
+ export declare function disableFunctionSpanTracking(): void;
16
+ /**
17
+ * Check if function span tracking is currently enabled
18
+ */
19
+ export declare function isFunctionSpanTrackingEnabled(): boolean;
20
+ /**
21
+ * Get the current funcspan header name and value
22
+ * Header constants are defined here but used by HTTP interceptors in index.tsx
23
+ * Checks both the enabled flag AND the expiration time before returning the header
24
+ */
25
+ export declare function getFuncSpanHeader(): {
26
+ name: string;
27
+ value: string;
28
+ } | null;
package/dist/websocket.js CHANGED
@@ -8,10 +8,21 @@ import version from "./version";
8
8
  const DEBUG = readDebugFlag(); // A wrapper around fetch that suppresses connection refused errors
9
9
  const MAX_MESSAGE_SIZE_MB = 50;
10
10
  const MAX_MESSAGE_SIZE_BYTES = MAX_MESSAGE_SIZE_MB * 1024 * 1024;
11
+ // Function span tracking header constants
12
+ const FUNCSPAN_HEADER_NAME = "X-Sf3-FunctionSpanCaptureOverride";
13
+ const FUNCSPAN_HEADER_VALUE = "1-0-5-5-0-1.0";
14
+ // Tracking configuration type constants (must match backend TrackingConfigurationType enum)
15
+ const TRACKING_CONFIG_GLOBAL = "global";
16
+ const TRACKING_CONFIG_PER_SESSION = "per_session";
11
17
  let webSocket = null;
12
18
  let isDraining = false;
13
19
  let inFlightFlush = null;
14
20
  let flushIntervalId = null;
21
+ // Function span tracking state (only manages enabled/disabled)
22
+ let funcSpanTrackingEnabled = false;
23
+ let funcSpanTimeoutId = null;
24
+ let funcSpanExpirationTime = null; // Timestamp when tracking should expire (milliseconds)
25
+ let isLocalTrackingMode = false; // True when tracking is enabled locally (Report Issue), not globally
15
26
  function isWebSocketOpen(ws) {
16
27
  return ws?.readyState === WebSocket.OPEN;
17
28
  }
@@ -49,10 +60,15 @@ export async function flushBufferedEvents() {
49
60
  for (const batch of idbBatches) {
50
61
  if (!isWebSocketOpen(webSocket))
51
62
  break;
52
- const eventsToSend = batch.map((e) => ({
53
- ...e.data,
54
- appUrl: e.data?.appUrl ?? window?.location?.href,
55
- }));
63
+ const eventsToSend = batch.map((e) => {
64
+ const event = {
65
+ ...e.data,
66
+ appUrl: e.data?.appUrl ?? window?.location?.href,
67
+ };
68
+ // Note: We do NOT add funcspan header to websocket events
69
+ // The header is only added to HTTP network requests
70
+ return event;
71
+ });
56
72
  const idsToDelete = batch
57
73
  .map((e) => e.id)
58
74
  .filter((id) => id != null);
@@ -81,6 +97,9 @@ export function sendEvent(event) {
81
97
  ...event,
82
98
  app_url: event?.app_url ?? window?.location?.href,
83
99
  };
100
+ // Note: We do NOT add funcspan header to websocket events
101
+ // The header is only added to HTTP network requests (fetch/XMLHttpRequest)
102
+ // This is handled in index.tsx injectHeader function
84
103
  if (isDraining || !isWebSocketOpen(webSocket)) {
85
104
  saveEventToIDB(enrichedEvent);
86
105
  return;
@@ -107,8 +126,10 @@ export function initializeWebSocket(backendApi, apiKey, sessionId) {
107
126
  };
108
127
  webSocket = new ReconnectingWebSocket(wsUrl, [], options);
109
128
  webSocket.addEventListener("open", () => {
110
- if (DEBUG)
111
- console.log("WebSocket opened.");
129
+ if (DEBUG) {
130
+ console.log("[Sailfish] WebSocket connection opened");
131
+ console.log(`[Sailfish] Function span tracking state: ${funcSpanTrackingEnabled ? 'ENABLED' : 'DISABLED'}`);
132
+ }
112
133
  (async () => {
113
134
  try {
114
135
  isDraining = true; // begin drain (blocks live sends)
@@ -128,7 +149,109 @@ export function initializeWebSocket(backendApi, apiKey, sessionId) {
128
149
  })();
129
150
  });
130
151
  webSocket.addEventListener("close", () => {
131
- console.log("WebSocket closed.");
152
+ if (DEBUG)
153
+ console.log("[Sailfish] WebSocket closed");
154
+ });
155
+ webSocket.addEventListener("message", (event) => {
156
+ try {
157
+ const data = JSON.parse(event.data);
158
+ // Handle function span tracking control messages from backend
159
+ if (data.type === "funcSpanTrackingControl") {
160
+ if (DEBUG) {
161
+ console.log(`[Sailfish] Received funcSpanTrackingControl message:`, {
162
+ enabled: data.enabled,
163
+ timeoutSeconds: data.timeoutSeconds,
164
+ expirationTimestampMs: data.expirationTimestampMs
165
+ });
166
+ }
167
+ // Clear any existing timeout
168
+ if (funcSpanTimeoutId !== null) {
169
+ window.clearTimeout(funcSpanTimeoutId);
170
+ funcSpanTimeoutId = null;
171
+ }
172
+ // This is a GLOBAL tracking control message
173
+ funcSpanTrackingEnabled = data.enabled;
174
+ isLocalTrackingMode = false; // Mark as global tracking, not local
175
+ if (DEBUG) {
176
+ console.log(`[Sailfish] Function span tracking ${data.enabled ? 'ENABLED (GLOBAL)' : 'DISABLED (GLOBAL)'}`);
177
+ }
178
+ if (data.enabled) {
179
+ // Use server-provided expiration timestamp for synchronization across all clients/pods
180
+ // This ensures all clients expire at the same time, even if they receive the message at different times
181
+ if (data.expirationTimestampMs) {
182
+ funcSpanExpirationTime = data.expirationTimestampMs;
183
+ // Calculate how many milliseconds until expiration
184
+ const now = Date.now();
185
+ const msUntilExpiration = funcSpanExpirationTime - now;
186
+ if (DEBUG) {
187
+ console.log(`[Sailfish] Server expiration timestamp: ${funcSpanExpirationTime}, ms until expiration: ${msUntilExpiration}`);
188
+ }
189
+ if (msUntilExpiration > 0) {
190
+ funcSpanTimeoutId = window.setTimeout(() => {
191
+ // Only auto-disable if still in global mode
192
+ if (!isLocalTrackingMode) {
193
+ funcSpanTrackingEnabled = false;
194
+ funcSpanExpirationTime = null;
195
+ if (DEBUG) {
196
+ console.log(`[Sailfish] GLOBAL function span tracking auto-disabled at server expiration time`);
197
+ }
198
+ }
199
+ }, msUntilExpiration);
200
+ }
201
+ else {
202
+ // Already expired
203
+ funcSpanTrackingEnabled = false;
204
+ funcSpanExpirationTime = null;
205
+ if (DEBUG) {
206
+ console.log(`[Sailfish] Tracking already expired, not enabling`);
207
+ }
208
+ }
209
+ }
210
+ else {
211
+ // Fallback: no server timestamp provided, use local calculation (legacy behavior)
212
+ const timeoutSeconds = data.timeoutSeconds || 3600; // Default 1 hour
213
+ if (timeoutSeconds > 0) {
214
+ funcSpanExpirationTime = Date.now() + (timeoutSeconds * 1000);
215
+ funcSpanTimeoutId = window.setTimeout(() => {
216
+ if (!isLocalTrackingMode) {
217
+ funcSpanTrackingEnabled = false;
218
+ funcSpanExpirationTime = null;
219
+ if (DEBUG) {
220
+ console.log(`[Sailfish] GLOBAL function span tracking auto-disabled after ${timeoutSeconds}s (legacy)`);
221
+ }
222
+ }
223
+ }, timeoutSeconds * 1000);
224
+ }
225
+ }
226
+ // Immediately report this session ID back to backend (non-blocking)
227
+ // This allows backend to track which sessions are active during tracking
228
+ try {
229
+ const sessionId = getOrSetSessionId();
230
+ webSocket.send(JSON.stringify({
231
+ type: "funcSpanTrackingSessionReport",
232
+ sessionId: sessionId,
233
+ enabled: true,
234
+ configurationType: TRACKING_CONFIG_GLOBAL
235
+ }));
236
+ if (DEBUG) {
237
+ console.log(`[Sailfish] GLOBAL tracking session report sent for session: ${sessionId}`);
238
+ }
239
+ }
240
+ catch (e) {
241
+ if (DEBUG) {
242
+ console.warn(`[Sailfish] Failed to send GLOBAL tracking session report:`, e);
243
+ }
244
+ }
245
+ }
246
+ else {
247
+ // When disabled, clear expiration time
248
+ funcSpanExpirationTime = null;
249
+ }
250
+ }
251
+ }
252
+ catch (e) {
253
+ // Ignore parse errors for non-JSON messages
254
+ }
132
255
  });
133
256
  return webSocket;
134
257
  }
@@ -157,3 +280,117 @@ function getWebSocketHost(url) {
157
280
  parser.href = url;
158
281
  return `${parser.hostname}${parser.port ? `:${parser.port}` : ""}`;
159
282
  }
283
+ /**
284
+ * Enable function span tracking for this session (e.g., when report issue recording starts)
285
+ * This is LOCAL tracking mode - only this session, not global
286
+ */
287
+ export function enableFunctionSpanTracking() {
288
+ if (DEBUG) {
289
+ console.log("[Sailfish] enableFunctionSpanTracking() called - Report Issue recording started (LOCAL MODE)");
290
+ }
291
+ funcSpanTrackingEnabled = true;
292
+ isLocalTrackingMode = true; // Mark as local tracking
293
+ funcSpanExpirationTime = null; // Local mode has no expiration
294
+ // Clear any existing timeout
295
+ if (funcSpanTimeoutId !== null) {
296
+ window.clearTimeout(funcSpanTimeoutId);
297
+ funcSpanTimeoutId = null;
298
+ }
299
+ // Report this session to backend for tracking with per_session configuration type
300
+ if (isWebSocketOpen(webSocket)) {
301
+ try {
302
+ const sessionId = getOrSetSessionId();
303
+ const message = {
304
+ type: "funcSpanTrackingSessionReport",
305
+ sessionId: sessionId,
306
+ enabled: true,
307
+ configurationType: TRACKING_CONFIG_PER_SESSION,
308
+ };
309
+ webSocket.send(JSON.stringify(message));
310
+ }
311
+ catch (e) {
312
+ console.error(`[FUNCSPAN START] ✗ Failed to send tracking session report:`, e);
313
+ }
314
+ }
315
+ else {
316
+ if (DEBUG) {
317
+ console.warn(`[Sailfish] WebSocket not open, cannot report LOCAL tracking session`);
318
+ }
319
+ }
320
+ }
321
+ /**
322
+ * Disable function span tracking for this session (e.g., when report issue recording stops)
323
+ * Only disables if we're in LOCAL tracking mode
324
+ */
325
+ export function disableFunctionSpanTracking() {
326
+ if (DEBUG) {
327
+ console.log("[Sailfish] disableFunctionSpanTracking() called - Report Issue recording stopped");
328
+ }
329
+ // Always send the disable message for per-session tracking
330
+ // This will only disable tracking for THIS specific recording session
331
+ // Global tracking (if active) will remain active
332
+ if (isWebSocketOpen(webSocket)) {
333
+ try {
334
+ const sessionId = getOrSetSessionId();
335
+ const message = {
336
+ type: "funcSpanTrackingSessionReport",
337
+ sessionId: sessionId,
338
+ enabled: false,
339
+ configurationType: TRACKING_CONFIG_PER_SESSION,
340
+ };
341
+ webSocket.send(JSON.stringify(message));
342
+ }
343
+ catch (e) {
344
+ console.error(`[FUNCSPAN STOP] ✗ Failed to send tracking stop report:`, e);
345
+ }
346
+ }
347
+ else {
348
+ console.warn(`[FUNCSPAN STOP] ✗ WebSocket not open, cannot notify tracking end`);
349
+ }
350
+ if (isLocalTrackingMode) {
351
+ funcSpanTrackingEnabled = false;
352
+ isLocalTrackingMode = false;
353
+ funcSpanExpirationTime = null;
354
+ if (DEBUG) {
355
+ console.log("[Sailfish] LOCAL tracking mode disabled");
356
+ }
357
+ }
358
+ // Clear any existing timeout
359
+ if (funcSpanTimeoutId !== null) {
360
+ window.clearTimeout(funcSpanTimeoutId);
361
+ funcSpanTimeoutId = null;
362
+ }
363
+ }
364
+ /**
365
+ * Check if function span tracking is currently enabled
366
+ */
367
+ export function isFunctionSpanTrackingEnabled() {
368
+ return funcSpanTrackingEnabled;
369
+ }
370
+ /**
371
+ * Get the current funcspan header name and value
372
+ * Header constants are defined here but used by HTTP interceptors in index.tsx
373
+ * Checks both the enabled flag AND the expiration time before returning the header
374
+ */
375
+ export function getFuncSpanHeader() {
376
+ if (!funcSpanTrackingEnabled) {
377
+ return null;
378
+ }
379
+ // Check if tracking has expired (for global mode with timeout)
380
+ if (funcSpanExpirationTime !== null) {
381
+ const now = Date.now();
382
+ if (now >= funcSpanExpirationTime) {
383
+ // Tracking has expired, disable it immediately
384
+ funcSpanTrackingEnabled = false;
385
+ funcSpanExpirationTime = null;
386
+ if (DEBUG) {
387
+ console.log("[Sailfish] Function span tracking expired on header check - disabling now");
388
+ }
389
+ return null;
390
+ }
391
+ }
392
+ return {
393
+ name: FUNCSPAN_HEADER_NAME,
394
+ value: FUNCSPAN_HEADER_VALUE
395
+ };
396
+ }
package/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "@sailfish-ai/recorder",
3
- "version": "1.8.1",
3
+ "version": "1.8.7",
4
4
  "publishPublicly": true,
5
- "main": "dist/sailfish-recorder.cjs.js",
6
- "module": "dist/sailfish-recorder.es.js",
5
+ "type": "module",
6
+ "main": "dist/recorder.cjs",
7
+ "module": "dist/recorder.js",
7
8
  "types": "dist/types/index.d.ts",
9
+ "unpkg": "dist/recorder.umd.cjs",
10
+ "jsdelivr": "dist/recorder.umd.cjs",
11
+ "sideEffects": false,
8
12
  "scripts": {
9
13
  "dev": "vite",
10
14
  "build": "vite build && tsc --project tsconfig.json",
@@ -15,19 +19,25 @@
15
19
  "artifactregistry-login": "npx google-artifactregistry-auth",
16
20
  "publish-local": "npm publish --tag latest --registry http://localhost:4873 --//localhost:4873/:_authToken=none"
17
21
  },
18
- "files": [
19
- "dist"
20
- ],
21
22
  "exports": {
22
23
  ".": {
23
24
  "types": "./dist/types/index.d.ts",
24
- "import": "./dist/sailfish-recorder.es.js",
25
- "require": "./dist/sailfish-recorder.cjs.js",
26
- "default": "./dist/sailfish-recorder.es.js"
25
+ "import": "./dist/recorder.js",
26
+ "require": "./dist/recorder.cjs",
27
+ "default": "./dist/recorder.js"
28
+ },
29
+ "./*": {
30
+ "types": "./dist/types/*.d.ts",
31
+ "import": "./dist/*.js",
32
+ "require": "./dist/*.cjs"
27
33
  },
28
34
  "./package.json": "./package.json"
29
35
  },
36
+ "files": [
37
+ "dist"
38
+ ],
30
39
  "dependencies": {
40
+ "@sailfish-ai/sf-map-utils": "0.4.3",
31
41
  "@sailfish-rrweb/rrweb-plugin-console-record": "0.5.2",
32
42
  "@sailfish-rrweb/rrweb-record-only": "0.5.2",
33
43
  "@sailfish-rrweb/types": "0.5.2",