@sailfish-ai/recorder 1.8.12 → 1.8.15

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.
@@ -1,8 +1,9 @@
1
- import { CaptureSettingsResponse, CreateTriageResponse, GraphQLResponse, StartSessionResponse } from "./types";
1
+ import { CaptureSettingsResponse, CreateTriageResponse, FunctionSpanTrackingEnabledResponse, GraphQLResponse, StartSessionResponse } from "./types";
2
2
  export declare function sendGraphQLRequest<T>(operationName: string, query: string, variables: object, retries?: number, // Number of retries before giving up
3
3
  initialBackoff?: number, // Initial backoff in milliseconds
4
4
  backoffFactor?: number): Promise<GraphQLResponse<T>>;
5
5
  export declare function fetchCaptureSettings(apiKey: string, backendApi: string): Promise<GraphQLResponse<CaptureSettingsResponse>>;
6
+ export declare function fetchFunctionSpanTrackingEnabled(apiKey: string, backendApi: string): Promise<GraphQLResponse<FunctionSpanTrackingEnabledResponse>>;
6
7
  export declare function startRecordingSession(apiKey: string, recordingId: string, backendApi: string, serviceIdentifier: string, serviceVersion?: string, mapUuid?: string, gitSha?: string, library?: string, serviceAdditionalMetadata?: Record<string, any>): Promise<GraphQLResponse<StartSessionResponse>>;
7
8
  export declare function sendDomainsToNotPropagateHeaderTo(apiKey: string, domains: string[], backendApi: string): Promise<GraphQLResponse<void>>;
8
9
  export declare function createTriageFromRecorder(apiKey: string, backendApi: string, recordingSessionId: string, timestampStart: string, timestampEnd: string, description?: string): Promise<GraphQLResponse<CreateTriageResponse>>;
@@ -46,6 +46,9 @@ export interface GraphQLResponse<T> {
46
46
  export interface CaptureSettingsResponse {
47
47
  captureSettingsFromApiKey: CaptureSettings;
48
48
  }
49
+ export interface FunctionSpanTrackingEnabledResponse {
50
+ isFunctionSpanTrackingEnabledFromApiKey: boolean;
51
+ }
49
52
  export interface StartSessionResponse {
50
53
  startRecordingSession: {
51
54
  id: string;
@@ -1,4 +1,9 @@
1
1
  import ReconnectingWebSocket from "reconnecting-websocket";
2
+ /**
3
+ * Clear stale function span tracking state (called from index.tsx to validate localStorage)
4
+ * This disables tracking and clears localStorage if backend says tracking is not active
5
+ */
6
+ export declare function clearStaleFuncSpanState(): void;
2
7
  export declare function flushBufferedEvents(): Promise<void>;
3
8
  export declare function sendEvent(event: any): void;
4
9
  export declare function initializeWebSocket(backendApi: string, apiKey: string, sessionId: string, envValue?: string): ReconnectingWebSocket;
@@ -17,6 +22,11 @@ export declare function disableFunctionSpanTracking(): void;
17
22
  * Check if function span tracking is currently enabled
18
23
  */
19
24
  export declare function isFunctionSpanTrackingEnabled(): boolean;
25
+ /**
26
+ * Initialize function span tracking state from API response (during page load)
27
+ * This is called before the WebSocket connects to ensure headers are added from the start
28
+ */
29
+ export declare function initializeFunctionSpanTrackingFromApi(isEnabled: boolean): void;
20
30
  /**
21
31
  * Get the current funcspan header name and value
22
32
  * Header constants are defined here but used by HTTP interceptors in index.tsx
package/dist/websocket.js CHANGED
@@ -10,16 +10,28 @@ const MAX_MESSAGE_SIZE_MB = 50;
10
10
  const MAX_MESSAGE_SIZE_BYTES = MAX_MESSAGE_SIZE_MB * 1024 * 1024;
11
11
  // Function span tracking header constants
12
12
  const FUNCSPAN_HEADER_NAME = "X-Sf3-FunctionSpanCaptureOverride";
13
- const FUNCSPAN_HEADER_VALUE = "1-0-5-5-0-1.0";
13
+ const FUNCSPAN_HEADER_VALUE = "1-1-10-10-1-1.0-1-0-0";
14
14
  // Tracking configuration type constants (must match backend TrackingConfigurationType enum)
15
15
  const TRACKING_CONFIG_GLOBAL = "global";
16
16
  const TRACKING_CONFIG_PER_SESSION = "per_session";
17
- // localStorage key for persisting global function span tracking state
18
- const FUNCSPAN_GLOBAL_STATE_KEY = "sailfish_funcspan_global_state";
19
17
  let webSocket = null;
20
18
  let isDraining = false;
21
19
  let inFlightFlush = null;
22
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
26
+ // Saved global state when switching to local mode
27
+ let savedGlobalTimeoutId = null;
28
+ let savedGlobalExpirationTime = null;
29
+ // LocalStorage key for persisting global function span tracking state
30
+ const FUNCSPAN_STORAGE_KEY = "sailfish_funcspan_global_state";
31
+ /**
32
+ * Save global function span tracking state to localStorage
33
+ * This persists the state across page refreshes
34
+ */
23
35
  function saveGlobalFuncSpanState(enabled, expirationTimestampMs) {
24
36
  try {
25
37
  if (typeof localStorage === "undefined")
@@ -27,73 +39,73 @@ function saveGlobalFuncSpanState(enabled, expirationTimestampMs) {
27
39
  const state = {
28
40
  enabled,
29
41
  expirationTimestampMs,
42
+ savedAt: Date.now(),
30
43
  };
31
- localStorage.setItem(FUNCSPAN_GLOBAL_STATE_KEY, JSON.stringify(state));
44
+ localStorage.setItem(FUNCSPAN_STORAGE_KEY, JSON.stringify(state));
32
45
  if (DEBUG) {
33
- console.log(`[Sailfish] Saved global function span state to localStorage:`, state);
46
+ console.log("[Sailfish] Saved funcSpan state to localStorage:", state);
34
47
  }
35
48
  }
36
49
  catch (e) {
37
50
  if (DEBUG) {
38
- console.warn(`[Sailfish] Failed to save global function span state to localStorage:`, e);
51
+ console.warn("[Sailfish] Failed to save funcSpan state to localStorage:", e);
39
52
  }
40
53
  }
41
54
  }
55
+ /**
56
+ * Load global function span tracking state from localStorage
57
+ */
42
58
  function loadGlobalFuncSpanState() {
43
59
  try {
44
60
  if (typeof localStorage === "undefined")
45
61
  return null;
46
- const stored = localStorage.getItem(FUNCSPAN_GLOBAL_STATE_KEY);
62
+ const stored = localStorage.getItem(FUNCSPAN_STORAGE_KEY);
47
63
  if (!stored)
48
64
  return null;
49
65
  const state = JSON.parse(stored);
50
- // Check if the stored state has expired
51
- if (state.enabled && state.expirationTimestampMs) {
52
- const now = Date.now();
53
- if (now >= state.expirationTimestampMs) {
54
- // Expired - clear localStorage and return null
55
- if (DEBUG) {
56
- console.log(`[Sailfish] Stored global function span state has expired, clearing`);
57
- }
58
- localStorage.removeItem(FUNCSPAN_GLOBAL_STATE_KEY);
59
- return null;
60
- }
61
- }
62
66
  if (DEBUG) {
63
- console.log(`[Sailfish] Loaded global function span state from localStorage:`, state);
67
+ console.log("[Sailfish] Loaded funcSpan state from localStorage:", state);
64
68
  }
65
69
  return state;
66
70
  }
67
71
  catch (e) {
68
72
  if (DEBUG) {
69
- console.warn(`[Sailfish] Failed to load global function span state from localStorage:`, e);
73
+ console.warn("[Sailfish] Failed to load funcSpan state from localStorage:", e);
70
74
  }
71
75
  return null;
72
76
  }
73
77
  }
78
+ /**
79
+ * Clear global function span tracking state from localStorage
80
+ */
74
81
  function clearGlobalFuncSpanState() {
75
82
  try {
76
83
  if (typeof localStorage === "undefined")
77
84
  return;
78
- localStorage.removeItem(FUNCSPAN_GLOBAL_STATE_KEY);
85
+ localStorage.removeItem(FUNCSPAN_STORAGE_KEY);
79
86
  if (DEBUG) {
80
- console.log(`[Sailfish] Cleared global function span state from localStorage`);
87
+ console.log("[Sailfish] Cleared funcSpan state from localStorage");
81
88
  }
82
89
  }
83
90
  catch (e) {
84
91
  if (DEBUG) {
85
- console.warn(`[Sailfish] Failed to clear global function span state from localStorage:`, e);
92
+ console.warn("[Sailfish] Failed to clear funcSpan state from localStorage:", e);
86
93
  }
87
94
  }
88
95
  }
89
- // Function span tracking state (only manages enabled/disabled)
90
- let funcSpanTrackingEnabled = false;
91
- let funcSpanTimeoutId = null;
92
- let funcSpanExpirationTime = null; // Timestamp when tracking should expire (milliseconds)
93
- let isLocalTrackingMode = false; // True when tracking is enabled locally (Report Issue), not globally
94
- // Saved global state when switching to local mode
95
- let savedGlobalTimeoutId = null;
96
- let savedGlobalExpirationTime = null;
96
+ /**
97
+ * Clear stale function span tracking state (called from index.tsx to validate localStorage)
98
+ * This disables tracking and clears localStorage if backend says tracking is not active
99
+ */
100
+ export function clearStaleFuncSpanState() {
101
+ funcSpanTrackingEnabled = false;
102
+ funcSpanExpirationTime = null;
103
+ isLocalTrackingMode = false;
104
+ clearGlobalFuncSpanState();
105
+ if (DEBUG) {
106
+ console.log("[Sailfish] Cleared stale function span tracking state (backend validation failed)");
107
+ }
108
+ }
97
109
  // Load persisted global state immediately on module initialization
98
110
  // This ensures headers are added from the very first HTTP request, even before WebSocket connects
99
111
  (() => {
@@ -105,9 +117,34 @@ let savedGlobalExpirationTime = null;
105
117
  if (DEBUG) {
106
118
  console.log(`[Sailfish] Module init: Restored global function span tracking from localStorage:`, {
107
119
  enabled: true,
108
- expirationTimestampMs: funcSpanExpirationTime,
120
+ expirationTime: funcSpanExpirationTime,
109
121
  });
110
122
  }
123
+ // Check if tracking has already expired
124
+ if (funcSpanExpirationTime !== null) {
125
+ const now = Date.now();
126
+ if (now >= funcSpanExpirationTime) {
127
+ // Already expired, clear it
128
+ funcSpanTrackingEnabled = false;
129
+ funcSpanExpirationTime = null;
130
+ clearGlobalFuncSpanState();
131
+ if (DEBUG) {
132
+ console.log("[Sailfish] Module init: Persisted tracking already expired, cleared state");
133
+ }
134
+ }
135
+ else {
136
+ // State is still valid, keep it temporarily until WebSocket provides the true state
137
+ if (DEBUG) {
138
+ console.log("[Sailfish] Module init: Function span tracking is active and valid (temporary until WebSocket confirms)");
139
+ }
140
+ }
141
+ }
142
+ else {
143
+ // No expiration time - keep enabled temporarily until WebSocket provides the true state
144
+ if (DEBUG) {
145
+ console.log("[Sailfish] Module init: Function span tracking is active (no expiration, temporary until WebSocket confirms)");
146
+ }
147
+ }
111
148
  }
112
149
  })();
113
150
  function isWebSocketOpen(ws) {
@@ -204,9 +241,6 @@ export function sendEvent(event) {
204
241
  }
205
242
  }
206
243
  export function initializeWebSocket(backendApi, apiKey, sessionId, envValue) {
207
- // Note: Global function span tracking state is now loaded at module initialization time
208
- // (see IIFE above) so headers work immediately, even before WebSocket connects.
209
- // The WebSocket "funcSpanTrackingControl" message will update the state if needed.
210
244
  const wsHost = getWebSocketHost(backendApi);
211
245
  const apiProtocol = new URL(backendApi).protocol;
212
246
  const wsScheme = apiProtocol === "https:" ? "wss" : "ws";
@@ -222,7 +256,7 @@ export function initializeWebSocket(backendApi, apiKey, sessionId, envValue) {
222
256
  webSocket.addEventListener("open", () => {
223
257
  if (DEBUG) {
224
258
  console.log("[Sailfish] WebSocket connection opened");
225
- console.log(`[Sailfish] Function span tracking state: ${funcSpanTrackingEnabled ? 'ENABLED' : 'DISABLED'}`);
259
+ console.log(`[Sailfish] Function span tracking state: ${funcSpanTrackingEnabled ? "ENABLED" : "DISABLED"}`);
226
260
  }
227
261
  (async () => {
228
262
  try {
@@ -255,7 +289,7 @@ export function initializeWebSocket(backendApi, apiKey, sessionId, envValue) {
255
289
  console.log(`[Sailfish] Received funcSpanTrackingControl message:`, {
256
290
  enabled: data.enabled,
257
291
  timeoutSeconds: data.timeoutSeconds,
258
- expirationTimestampMs: data.expirationTimestampMs
292
+ expirationTimestampMs: data.expirationTimestampMs,
259
293
  });
260
294
  }
261
295
  // Clear any existing timeout
@@ -267,7 +301,7 @@ export function initializeWebSocket(backendApi, apiKey, sessionId, envValue) {
267
301
  funcSpanTrackingEnabled = data.enabled;
268
302
  isLocalTrackingMode = false; // Mark as global tracking, not local
269
303
  if (DEBUG) {
270
- console.log(`[Sailfish] Function span tracking ${data.enabled ? 'ENABLED (GLOBAL)' : 'DISABLED (GLOBAL)'}`);
304
+ console.log(`[Sailfish] Function span tracking ${data.enabled ? "ENABLED (GLOBAL)" : "DISABLED (GLOBAL)"}`);
271
305
  }
272
306
  if (data.enabled) {
273
307
  // Use server-provided expiration timestamp for synchronization across all clients/pods
@@ -293,6 +327,25 @@ export function initializeWebSocket(backendApi, apiKey, sessionId, envValue) {
293
327
  if (DEBUG) {
294
328
  console.log(`[Sailfish] GLOBAL function span tracking auto-disabled at server expiration time`);
295
329
  }
330
+ // Notify backend that this client's tracking has expired
331
+ // Backend will update database and broadcast to all other clients
332
+ try {
333
+ if (isWebSocketOpen(webSocket)) {
334
+ webSocket.send(JSON.stringify({
335
+ type: "funcSpanTrackingExpired",
336
+ sessionId: getOrSetSessionId(),
337
+ expiredAt: Date.now(),
338
+ }));
339
+ if (DEBUG) {
340
+ console.log(`[Sailfish] Notified backend that function span tracking expired`);
341
+ }
342
+ }
343
+ }
344
+ catch (e) {
345
+ if (DEBUG) {
346
+ console.warn(`[Sailfish] Failed to notify backend of tracking expiry:`, e);
347
+ }
348
+ }
296
349
  }
297
350
  }, msUntilExpiration);
298
351
  }
@@ -311,7 +364,7 @@ export function initializeWebSocket(backendApi, apiKey, sessionId, envValue) {
311
364
  // Fallback: no server timestamp provided, use local calculation (legacy behavior)
312
365
  const timeoutSeconds = data.timeoutSeconds || 3600; // Default 1 hour
313
366
  if (timeoutSeconds > 0) {
314
- funcSpanExpirationTime = Date.now() + (timeoutSeconds * 1000);
367
+ funcSpanExpirationTime = Date.now() + timeoutSeconds * 1000;
315
368
  // Save to localStorage for persistence across page refreshes
316
369
  saveGlobalFuncSpanState(true, funcSpanExpirationTime);
317
370
  funcSpanTimeoutId = window.setTimeout(() => {
@@ -323,6 +376,24 @@ export function initializeWebSocket(backendApi, apiKey, sessionId, envValue) {
323
376
  if (DEBUG) {
324
377
  console.log(`[Sailfish] GLOBAL function span tracking auto-disabled after ${timeoutSeconds}s (legacy)`);
325
378
  }
379
+ // Notify backend that this client's tracking has expired
380
+ try {
381
+ if (isWebSocketOpen(webSocket)) {
382
+ webSocket.send(JSON.stringify({
383
+ type: "funcSpanTrackingExpired",
384
+ sessionId: getOrSetSessionId(),
385
+ expiredAt: Date.now(),
386
+ }));
387
+ if (DEBUG) {
388
+ console.log(`[Sailfish] Notified backend that function span tracking expired (legacy timeout)`);
389
+ }
390
+ }
391
+ }
392
+ catch (e) {
393
+ if (DEBUG) {
394
+ console.warn(`[Sailfish] Failed to notify backend of tracking expiry:`, e);
395
+ }
396
+ }
326
397
  }
327
398
  }, timeoutSeconds * 1000);
328
399
  }
@@ -335,7 +406,7 @@ export function initializeWebSocket(backendApi, apiKey, sessionId, envValue) {
335
406
  type: "funcSpanTrackingSessionReport",
336
407
  sessionId: sessionId,
337
408
  enabled: true,
338
- configurationType: TRACKING_CONFIG_GLOBAL
409
+ configurationType: TRACKING_CONFIG_GLOBAL,
339
410
  }));
340
411
  if (DEBUG) {
341
412
  console.log(`[Sailfish] GLOBAL tracking session report sent for session: ${sessionId}`);
@@ -395,18 +466,14 @@ export function enableFunctionSpanTracking() {
395
466
  if (DEBUG) {
396
467
  console.log("[Sailfish] enableFunctionSpanTracking() called - Report Issue recording started (LOCAL MODE)");
397
468
  }
398
- // If global tracking was already active, save its state so we can restore it later
399
- if (funcSpanTrackingEnabled && !isLocalTrackingMode) {
400
- savedGlobalTimeoutId = funcSpanTimeoutId;
401
- savedGlobalExpirationTime = funcSpanExpirationTime;
402
- if (DEBUG) {
403
- console.log("[Sailfish] Saved global tracking state while switching to local mode");
404
- }
405
- }
406
469
  funcSpanTrackingEnabled = true;
407
470
  isLocalTrackingMode = true; // Mark as local tracking
408
471
  funcSpanExpirationTime = null; // Local mode has no expiration
409
- funcSpanTimeoutId = null; // Clear timeout (saved above if it was global)
472
+ // Clear any existing timeout
473
+ if (funcSpanTimeoutId !== null) {
474
+ window.clearTimeout(funcSpanTimeoutId);
475
+ funcSpanTimeoutId = null;
476
+ }
410
477
  // Report this session to backend for tracking with per_session configuration type
411
478
  if (isWebSocketOpen(webSocket)) {
412
479
  try {
@@ -459,29 +526,17 @@ export function disableFunctionSpanTracking() {
459
526
  console.warn(`[FUNCSPAN STOP] ✗ WebSocket not open, cannot notify tracking end`);
460
527
  }
461
528
  if (isLocalTrackingMode) {
529
+ funcSpanTrackingEnabled = false;
462
530
  isLocalTrackingMode = false;
463
- // Restore global tracking state if it was active before local mode
464
- if (savedGlobalTimeoutId !== null || savedGlobalExpirationTime !== null) {
465
- funcSpanTrackingEnabled = true; // Keep tracking enabled (global was active)
466
- funcSpanExpirationTime = savedGlobalExpirationTime;
467
- funcSpanTimeoutId = savedGlobalTimeoutId;
468
- if (DEBUG) {
469
- console.log("[Sailfish] LOCAL tracking mode disabled, restored GLOBAL tracking state");
470
- }
471
- // Clear saved state
472
- savedGlobalTimeoutId = null;
473
- savedGlobalExpirationTime = null;
474
- }
475
- else {
476
- // No global tracking was active, fully disable
477
- funcSpanTrackingEnabled = false;
478
- funcSpanExpirationTime = null;
479
- funcSpanTimeoutId = null;
480
- if (DEBUG) {
481
- console.log("[Sailfish] LOCAL tracking mode disabled, no global tracking to restore");
482
- }
531
+ funcSpanExpirationTime = null;
532
+ if (DEBUG) {
533
+ console.log("[Sailfish] LOCAL tracking mode disabled");
483
534
  }
484
535
  }
536
+ if (funcSpanTimeoutId !== null) {
537
+ window.clearTimeout(funcSpanTimeoutId);
538
+ funcSpanTimeoutId = null;
539
+ }
485
540
  }
486
541
  /**
487
542
  * Check if function span tracking is currently enabled
@@ -489,6 +544,28 @@ export function disableFunctionSpanTracking() {
489
544
  export function isFunctionSpanTrackingEnabled() {
490
545
  return funcSpanTrackingEnabled;
491
546
  }
547
+ /**
548
+ * Initialize function span tracking state from API response (during page load)
549
+ * This is called before the WebSocket connects to ensure headers are added from the start
550
+ */
551
+ export function initializeFunctionSpanTrackingFromApi(isEnabled) {
552
+ if (isEnabled && !funcSpanTrackingEnabled) {
553
+ funcSpanTrackingEnabled = true;
554
+ isLocalTrackingMode = false; // This is global tracking from API
555
+ funcSpanExpirationTime = null; // Will be set by WebSocket message later
556
+ if (DEBUG) {
557
+ console.log("[Sailfish] Function span tracking initialized as ENABLED from API check");
558
+ }
559
+ }
560
+ else if (!isEnabled && funcSpanTrackingEnabled) {
561
+ funcSpanTrackingEnabled = false;
562
+ isLocalTrackingMode = false;
563
+ funcSpanExpirationTime = null;
564
+ if (DEBUG) {
565
+ console.log("[Sailfish] Function span tracking initialized as DISABLED from API check");
566
+ }
567
+ }
568
+ }
492
569
  /**
493
570
  * Get the current funcspan header name and value
494
571
  * Header constants are defined here but used by HTTP interceptors in index.tsx
@@ -505,6 +582,7 @@ export function getFuncSpanHeader() {
505
582
  // Tracking has expired, disable it immediately
506
583
  funcSpanTrackingEnabled = false;
507
584
  funcSpanExpirationTime = null;
585
+ clearGlobalFuncSpanState();
508
586
  if (DEBUG) {
509
587
  console.log("[Sailfish] Function span tracking expired on header check - disabling now");
510
588
  }
@@ -513,6 +591,6 @@ export function getFuncSpanHeader() {
513
591
  }
514
592
  return {
515
593
  name: FUNCSPAN_HEADER_NAME,
516
- value: FUNCSPAN_HEADER_VALUE
594
+ value: FUNCSPAN_HEADER_VALUE,
517
595
  };
518
596
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailfish-ai/recorder",
3
- "version": "1.8.12",
3
+ "version": "1.8.15",
4
4
  "publishPublicly": true,
5
5
  "type": "module",
6
6
  "main": "dist/recorder.cjs",
@@ -37,7 +37,7 @@
37
37
  "dist"
38
38
  ],
39
39
  "dependencies": {
40
- "@sailfish-ai/sf-map-utils": "0.4.4",
40
+ "@sailfish-ai/sf-map-utils": "0.4.5",
41
41
  "@sailfish-rrweb/rrweb-plugin-console-record": "0.5.2",
42
42
  "@sailfish-rrweb/rrweb-record-only": "0.5.2",
43
43
  "@sailfish-rrweb/types": "0.5.2",