@sailfish-ai/recorder 1.7.42 → 1.7.47

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,4 +1,26 @@
1
+ type ShortcutConfig = {
2
+ key: string;
3
+ requireCmdCtrl?: boolean;
4
+ };
5
+ export type ShortcutsConfig = {
6
+ enabled: boolean;
7
+ openModalExistingMode: ShortcutConfig;
8
+ openModalCaptureNewMode: ShortcutConfig;
9
+ closeModal: ShortcutConfig;
10
+ submitReport: ShortcutConfig;
11
+ startRecording: ShortcutConfig;
12
+ stopRecording: ShortcutConfig;
13
+ };
1
14
  export declare const ReportIssueContext: {
15
+ shortcuts: {
16
+ enabled: boolean;
17
+ openModalExistingMode: ShortcutConfig;
18
+ openModalCaptureNewMode: ShortcutConfig;
19
+ closeModal: ShortcutConfig;
20
+ submitReport: ShortcutConfig;
21
+ startRecording: ShortcutConfig;
22
+ stopRecording: ShortcutConfig;
23
+ };
2
24
  resolveSessionId: (() => string) | null;
3
25
  apiKey: string | null;
4
26
  backendApi: string | null;
@@ -8,6 +30,7 @@ export declare function setupIssueReporting(options: {
8
30
  apiKey: string;
9
31
  backendApi: string;
10
32
  getSessionId: () => string;
11
- enableShortcuts?: boolean;
33
+ shortcuts?: Partial<ShortcutsConfig>;
12
34
  }): void;
13
35
  export declare function openReportIssueModal(customBaseUrl?: string): void;
36
+ export {};
@@ -1,4 +1,5 @@
1
1
  import { LogRecordOptions } from "@sailfish-rrweb/rrweb-plugin-console-record";
2
+ import { ShortcutsConfig } from "./inAppReportIssueModal";
2
3
  import { CaptureSettings } from "./types";
3
4
  export declare const STORAGE_VERSION = 1;
4
5
  export declare const DYNAMIC_PASSED_HOSTS_KEY = "dynamicPassedHosts";
@@ -30,7 +31,7 @@ export declare const initRecorder: (options: {
30
31
  backendApi?: string;
31
32
  domainsToPropagateHeaderTo?: string[];
32
33
  domainsToNotPropagateHeaderTo?: string[];
33
- enableShortcuts?: boolean;
34
+ reportIssueShortcuts?: Partial<ShortcutsConfig>;
34
35
  serviceVersion?: string;
35
36
  serviceIdentifier?: string;
36
37
  gitSha?: string;
package/dist/websocket.js CHANGED
@@ -7,6 +7,9 @@ const MAX_MESSAGE_SIZE_MB = 50;
7
7
  const MAX_MESSAGE_SIZE_BYTES = MAX_MESSAGE_SIZE_MB * 1024 * 1024;
8
8
  const DEBUG = import.meta.env.VITE_DEBUG ? import.meta.env.VITE_DEBUG : false;
9
9
  let webSocket = null;
10
+ let isDraining = false;
11
+ let inFlightFlush = null;
12
+ let flushIntervalId = null;
10
13
  function isWebSocketOpen(ws) {
11
14
  return ws?.readyState === WebSocket.OPEN;
12
15
  }
@@ -25,51 +28,64 @@ async function flushNotifyQueue() {
25
28
  export async function flushBufferedEvents() {
26
29
  if (!isWebSocketOpen(webSocket))
27
30
  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);
31
+ // ensure only one flush at a time
32
+ if (inFlightFlush) {
33
+ await inFlightFlush;
34
+ return;
35
35
  }
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);
36
+ inFlightFlush = (async () => {
37
+ const persisted = await getAllIndexedEvents();
38
+ const groupedBySession = {};
39
+ for (const event of persisted) {
40
+ const sessionId = event?.data?.data?.sessionId ?? "unknown-session";
41
+ if (!groupedBySession[sessionId])
42
+ groupedBySession[sessionId] = [];
43
+ groupedBySession[sessionId].push(event);
44
+ }
45
+ for (const groupedEvents of Object.values(groupedBySession)) {
46
+ const idbBatches = buildBatches(groupedEvents, (e) => eventSize(e.data), MAX_MESSAGE_SIZE_BYTES);
47
+ for (const batch of idbBatches) {
48
+ if (!isWebSocketOpen(webSocket))
49
+ break;
50
+ const eventsToSend = batch.map((e) => e.data);
51
+ const idsToDelete = batch
52
+ .map((e) => e.id)
53
+ .filter((id) => id != null);
54
+ try {
55
+ const message = JSON.stringify({
56
+ type: "events",
57
+ events: eventsToSend,
58
+ mapUuid: window.sfMapUuid,
59
+ });
60
+ webSocket.send(message);
61
+ await deleteEventsByIds(idsToDelete);
62
+ }
63
+ catch (err) { }
53
64
  }
54
- catch (err) { }
55
65
  }
66
+ })();
67
+ try {
68
+ await inFlightFlush;
69
+ }
70
+ finally {
71
+ inFlightFlush = null;
56
72
  }
57
73
  }
58
74
  export function sendEvent(event) {
75
+ // while draining, never send live—persist so flush preserves order
76
+ if (isDraining || !isWebSocketOpen(webSocket)) {
77
+ saveEventToIDB(event);
78
+ return;
79
+ }
59
80
  const msg = JSON.stringify({
60
81
  type: "event",
61
82
  event,
62
83
  mapUuid: window.sfMapUuid,
63
84
  });
64
- if (isWebSocketOpen(webSocket)) {
65
- try {
66
- webSocket.send(msg);
67
- }
68
- catch (err) {
69
- saveEventToIDB(event);
70
- }
85
+ try {
86
+ webSocket.send(msg);
71
87
  }
72
- else {
88
+ catch (err) {
73
89
  saveEventToIDB(event);
74
90
  }
75
91
  }
@@ -86,8 +102,21 @@ export function initializeWebSocket(backendApi, apiKey, sessionId) {
86
102
  if (DEBUG)
87
103
  console.log("WebSocket opened.");
88
104
  (async () => {
89
- await flushNotifyQueue();
90
- setInterval(() => flushBufferedEvents(), 10000);
105
+ try {
106
+ isDraining = true; // begin drain (blocks live sends)
107
+ await flushNotifyQueue(); // notify messages can go first
108
+ await flushBufferedEvents(); // ordered drain of IDB events
109
+ }
110
+ finally {
111
+ isDraining = false; // resume live sends
112
+ }
113
+ // start periodic top-up flush after initial drain
114
+ if (flushIntervalId != null)
115
+ clearInterval(flushIntervalId);
116
+ flushIntervalId = window.setInterval(() => {
117
+ // fire-and-forget; single-flight guard prevents overlaps
118
+ void flushBufferedEvents();
119
+ }, 2000);
91
120
  })();
92
121
  });
93
122
  webSocket.addEventListener("close", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailfish-ai/recorder",
3
- "version": "1.7.42",
3
+ "version": "1.7.47",
4
4
  "publishPublicly": true,
5
5
  "main": "dist/sailfish-recorder.umd.js",
6
6
  "types": "dist/types/index.d.ts",
@@ -39,7 +39,7 @@
39
39
  "devDependencies": {
40
40
  "@rollup/plugin-terser": "^0.4.4",
41
41
  "@types/jest": "^30.0.0",
42
- "@types/node": "^18.0.0",
42
+ "@types/node": "^20.19.10",
43
43
  "@types/react": "^19.1.8",
44
44
  "@types/react-dom": "^19.1.6",
45
45
  "@types/uuid": "^10.0.0",