@sailfish-ai/recorder 1.7.46 → 1.7.49

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
@@ -0,0 +1,12 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import { HAS_WINDOW } from "./runtimeEnv";
3
+ // Storing the sailfishSessionId in window.name, as window.name retains its value after a page refresh
4
+ // but resets when a new tab (including a duplicated tab) is opened.
5
+ export function getOrSetSessionId() {
6
+ if (!HAS_WINDOW)
7
+ return uuidv4();
8
+ if (!window.name) {
9
+ window.name = uuidv4();
10
+ }
11
+ return window.name;
12
+ }
@@ -25,12 +25,14 @@ export declare const ReportIssueContext: {
25
25
  apiKey: string | null;
26
26
  backendApi: string | null;
27
27
  triageBaseUrl: string;
28
+ deactivateIsolation: () => void;
28
29
  };
29
30
  export declare function setupIssueReporting(options: {
30
31
  apiKey: string;
31
32
  backendApi: string;
32
33
  getSessionId: () => string;
33
34
  shortcuts?: Partial<ShortcutsConfig>;
35
+ customBaseUrl?: string;
34
36
  }): void;
35
- export declare function openReportIssueModal(customBaseUrl?: string): void;
37
+ export declare function openReportIssueModal(): void;
36
38
  export {};
@@ -2,17 +2,6 @@ import { LogRecordOptions } from "@sailfish-rrweb/rrweb-plugin-console-record";
2
2
  import { ShortcutsConfig } from "./inAppReportIssueModal";
3
3
  import { CaptureSettings } from "./types";
4
4
  export declare const STORAGE_VERSION = 1;
5
- export declare const DYNAMIC_PASSED_HOSTS_KEY = "dynamicPassedHosts";
6
- export declare const DYNAMIC_EXCLUDED_HOSTS_KEY = "dynamicExcludedHosts";
7
- export declare const dynamicExcludedHosts: Set<string>;
8
- export declare const dynamicPassedHosts: Set<string>;
9
- /**
10
- * Given a set of excluded hostPaths (like "foo.com/bar", "foo.com/baz", etc.),
11
- * find any path prefixes under each host that appear ≥ threshold times,
12
- * and replace their individual entries with a single wildcard pattern
13
- * (e.g. "foo.com/bar*").
14
- */
15
- export declare function consolidateDynamicExclusions(hostPathSet: Set<string>, threshold?: number): Set<string>;
16
5
  export declare const DEFAULT_CAPTURE_SETTINGS: CaptureSettings;
17
6
  export declare const DEFAULT_CONSOLE_RECORDING_SETTINGS: LogRecordOptions;
18
7
  export declare function matchUrlWithWildcard(input: unknown, patterns: string[]): boolean;
@@ -36,6 +25,7 @@ export declare const initRecorder: (options: {
36
25
  serviceIdentifier?: string;
37
26
  gitSha?: string;
38
27
  serviceAdditionalMetadata?: Record<string, any>;
28
+ customBaseUrl?: string;
39
29
  }) => Promise<void>;
40
30
  export * from "./graphql";
41
31
  export { openReportIssueModal } from "./inAppReportIssueModal";
@@ -0,0 +1,4 @@
1
+ export declare const HAS_WINDOW: boolean;
2
+ export declare const HAS_DOCUMENT: boolean;
3
+ export declare const HAS_LOCAL_STORAGE: boolean;
4
+ export declare const HAS_SESSION_STORAGE: boolean;
@@ -0,0 +1 @@
1
+ export declare function getOrSetSessionId(): string;
package/dist/websocket.js CHANGED
@@ -1,12 +1,16 @@
1
1
  import ReconnectingWebSocket from "reconnecting-websocket";
2
2
  import { deleteEventsByIds, getAllIndexedEvents, saveEventToIDB, } from "./eventStore";
3
3
  import { deleteNotifyMessageById, getAllNotifyMessages, saveNotifyMessageToIDB, } from "./notifyEventStore";
4
+ import { getOrSetSessionId } from "./session";
4
5
  import { buildBatches, eventSize } from "./utils";
5
6
  import version from "./version";
6
7
  const MAX_MESSAGE_SIZE_MB = 50;
7
8
  const MAX_MESSAGE_SIZE_BYTES = MAX_MESSAGE_SIZE_MB * 1024 * 1024;
8
9
  const DEBUG = import.meta.env.VITE_DEBUG ? import.meta.env.VITE_DEBUG : false;
9
10
  let webSocket = null;
11
+ let isDraining = false;
12
+ let inFlightFlush = null;
13
+ let flushIntervalId = null;
10
14
  function isWebSocketOpen(ws) {
11
15
  return ws?.readyState === WebSocket.OPEN;
12
16
  }
@@ -25,51 +29,64 @@ async function flushNotifyQueue() {
25
29
  export async function flushBufferedEvents() {
26
30
  if (!isWebSocketOpen(webSocket))
27
31
  return;
28
- const persisted = await getAllIndexedEvents();
29
- const groupedBySession = {};
30
- for (const event of persisted) {
31
- const sessionId = event?.data?.data?.sessionId ?? "unknown-session";
32
- if (!groupedBySession[sessionId])
33
- groupedBySession[sessionId] = [];
34
- groupedBySession[sessionId].push(event);
32
+ // ensure only one flush at a time
33
+ if (inFlightFlush) {
34
+ await inFlightFlush;
35
+ return;
35
36
  }
36
- for (const groupedEvents of Object.values(groupedBySession)) {
37
- const idbBatches = buildBatches(groupedEvents, (e) => eventSize(e.data), MAX_MESSAGE_SIZE_BYTES);
38
- for (const batch of idbBatches) {
39
- if (!isWebSocketOpen(webSocket))
40
- break;
41
- const eventsToSend = batch.map((e) => e.data);
42
- const idsToDelete = batch
43
- .map((e) => e.id)
44
- .filter((id) => id != null);
45
- try {
46
- const message = JSON.stringify({
47
- type: "events",
48
- events: eventsToSend,
49
- mapUuid: window.sfMapUuid,
50
- });
51
- webSocket.send(message);
52
- await deleteEventsByIds(idsToDelete);
37
+ inFlightFlush = (async () => {
38
+ const persisted = await getAllIndexedEvents();
39
+ const groupedBySession = {};
40
+ for (const event of persisted) {
41
+ const sessionId = event?.data?.data?.sessionId ?? "unknown-session";
42
+ if (!groupedBySession[sessionId])
43
+ groupedBySession[sessionId] = [];
44
+ groupedBySession[sessionId].push(event);
45
+ }
46
+ for (const groupedEvents of Object.values(groupedBySession)) {
47
+ const idbBatches = buildBatches(groupedEvents, (e) => eventSize(e.data), MAX_MESSAGE_SIZE_BYTES);
48
+ for (const batch of idbBatches) {
49
+ if (!isWebSocketOpen(webSocket))
50
+ break;
51
+ const eventsToSend = batch.map((e) => e.data);
52
+ const idsToDelete = batch
53
+ .map((e) => e.id)
54
+ .filter((id) => id != null);
55
+ try {
56
+ const message = JSON.stringify({
57
+ type: "events",
58
+ events: eventsToSend,
59
+ mapUuid: window.sfMapUuid,
60
+ });
61
+ webSocket.send(message);
62
+ await deleteEventsByIds(idsToDelete);
63
+ }
64
+ catch (err) { }
53
65
  }
54
- catch (err) { }
55
66
  }
67
+ })();
68
+ try {
69
+ await inFlightFlush;
70
+ }
71
+ finally {
72
+ inFlightFlush = null;
56
73
  }
57
74
  }
58
75
  export function sendEvent(event) {
76
+ // while draining, never send live—persist so flush preserves order
77
+ if (isDraining || !isWebSocketOpen(webSocket)) {
78
+ saveEventToIDB(event);
79
+ return;
80
+ }
59
81
  const msg = JSON.stringify({
60
82
  type: "event",
61
83
  event,
62
84
  mapUuid: window.sfMapUuid,
63
85
  });
64
- if (isWebSocketOpen(webSocket)) {
65
- try {
66
- webSocket.send(msg);
67
- }
68
- catch (err) {
69
- saveEventToIDB(event);
70
- }
86
+ try {
87
+ webSocket.send(msg);
71
88
  }
72
- else {
89
+ catch (err) {
73
90
  saveEventToIDB(event);
74
91
  }
75
92
  }
@@ -86,8 +103,21 @@ export function initializeWebSocket(backendApi, apiKey, sessionId) {
86
103
  if (DEBUG)
87
104
  console.log("WebSocket opened.");
88
105
  (async () => {
89
- await flushNotifyQueue();
90
- setInterval(() => flushBufferedEvents(), 10000);
106
+ try {
107
+ isDraining = true; // begin drain (blocks live sends)
108
+ await flushNotifyQueue(); // notify messages can go first
109
+ await flushBufferedEvents(); // ordered drain of IDB events
110
+ }
111
+ finally {
112
+ isDraining = false; // resume live sends
113
+ }
114
+ // start periodic top-up flush after initial drain
115
+ if (flushIntervalId != null)
116
+ clearInterval(flushIntervalId);
117
+ flushIntervalId = window.setInterval(() => {
118
+ // fire-and-forget; single-flight guard prevents overlaps
119
+ void flushBufferedEvents();
120
+ }, 2000);
91
121
  })();
92
122
  });
93
123
  webSocket.addEventListener("close", () => {
@@ -96,6 +126,9 @@ export function initializeWebSocket(backendApi, apiKey, sessionId) {
96
126
  return webSocket;
97
127
  }
98
128
  export function sendMessage(message) {
129
+ if (!("sessionId" in message)) {
130
+ message.sessionId = getOrSetSessionId();
131
+ }
99
132
  const msg = JSON.stringify(message);
100
133
  if (isWebSocketOpen(webSocket)) {
101
134
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailfish-ai/recorder",
3
- "version": "1.7.46",
3
+ "version": "1.7.49",
4
4
  "publishPublicly": true,
5
5
  "main": "dist/sailfish-recorder.umd.js",
6
6
  "types": "dist/types/index.d.ts",