@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
package/dist/recording.js CHANGED
@@ -3,7 +3,8 @@ import { EventType } from "@sailfish-rrweb/types";
3
3
  import { Complete, DomContentEventId, DomContentSource, Loading, } from "./constants";
4
4
  import { getCallerLocation, getCallerLocationFromTrace, } from "./sourceLocation";
5
5
  import suppressConsoleLogsDuringCall from "./suppressConsoleLogsDuringCall";
6
- import { initializeWebSocket, sendEvent } from "./websocket";
6
+ import { yieldToMain } from "./scheduler";
7
+ import { getCachedHrefNoQuery, initializeWebSocket, sendEvent } from "./websocket";
7
8
  // Module-level reference to rrweb record, populated after dynamic import
8
9
  let _record = null;
9
10
  const MASK_CLASS = "sailfishSanitize";
@@ -82,13 +83,34 @@ function maskInputFn(text, node) {
82
83
  }
83
84
  return text;
84
85
  }
85
- export const getUrlAndStoredUuids = () => ({
86
- page_visit_uuid: sessionStorage.getItem("pageVisitUUID"),
87
- prev_page_visit_uuid: sessionStorage.getItem("prevPageVisitUUID"),
88
- href: location.origin + location.pathname,
89
- tabVisibilityChanged: sessionStorage.getItem("tabVisibilityChanged"),
90
- tabVisibilityState: sessionStorage.getItem("tabVisibilityState"),
91
- });
86
+ // Cached sessionStorage values avoids per-call sessionStorage reads on the hot path
87
+ let _ssCacheDirty = true;
88
+ let _ssPageVisitUUID = null;
89
+ let _ssPrevPageVisitUUID = null;
90
+ let _ssTabVisibilityChanged = null;
91
+ let _ssTabVisibilityState = null;
92
+ function _refreshSessionStorageCache() {
93
+ _ssPageVisitUUID = sessionStorage.getItem("pageVisitUUID");
94
+ _ssPrevPageVisitUUID = sessionStorage.getItem("prevPageVisitUUID");
95
+ _ssTabVisibilityChanged = sessionStorage.getItem("tabVisibilityChanged");
96
+ _ssTabVisibilityState = sessionStorage.getItem("tabVisibilityState");
97
+ _ssCacheDirty = false;
98
+ }
99
+ /** Call after writing to sessionStorage keys used by getUrlAndStoredUuids(). */
100
+ export function invalidateUrlCache() {
101
+ _ssCacheDirty = true;
102
+ }
103
+ export const getUrlAndStoredUuids = () => {
104
+ if (_ssCacheDirty)
105
+ _refreshSessionStorageCache();
106
+ return {
107
+ page_visit_uuid: _ssPageVisitUUID,
108
+ prev_page_visit_uuid: _ssPrevPageVisitUUID,
109
+ href: getCachedHrefNoQuery(),
110
+ tabVisibilityChanged: _ssTabVisibilityChanged,
111
+ tabVisibilityState: _ssTabVisibilityState,
112
+ };
113
+ };
92
114
  export function initializeDomContentEvents(sessionId) {
93
115
  document.addEventListener("readystatechange", () => {
94
116
  const timestamp = Date.now();
@@ -140,6 +162,8 @@ export function initializeDomContentEvents(sessionId) {
140
162
  }
141
163
  export async function initializeConsolePlugin(consoleRecordSettings, sessionId) {
142
164
  const { getRecordConsolePlugin } = await import("@sailfish-rrweb/rrweb-plugin-console-record");
165
+ // Yield between dynamic import and observer setup
166
+ await yieldToMain();
143
167
  const { name, observer } = getRecordConsolePlugin(consoleRecordSettings);
144
168
  observer((payload) => {
145
169
  const anyPayload = payload;
@@ -245,7 +269,7 @@ function createThrottledEmit(enabled, intervalMs = 1000) {
245
269
  }
246
270
  export async function initializeRecording(captureSettings, // TODO - Sibyl post-launch - replace type
247
271
  // networkRecordSettings: NetworkRecordOptions,
248
- backendApi, apiKey, sessionId, envValue, deferRecordingStart = true, useWsWorker = false) {
272
+ backendApi, apiKey, sessionId, envValue, deferRecordingStart = true, useWsWorker = false, chunkSnapshot = false) {
249
273
  const webSocket = initializeWebSocket(backendApi, apiKey, sessionId, envValue, useWsWorker);
250
274
  try {
251
275
  // Create throttled emit wrapper (only for text edit events)
@@ -272,15 +296,100 @@ backendApi, apiKey, sessionId, envValue, deferRecordingStart = true, useWsWorker
272
296
  }
273
297
  const { record } = await import("@sailfish-rrweb/rrweb-record-only");
274
298
  _record = record;
275
- record({
276
- emit(event) {
277
- emitWithContext(event);
278
- },
279
- maskInputOptions: { text: true },
280
- maskInputFn,
281
- maskTextClass: MASK_CLASS,
282
- ...captureSettings,
283
- });
299
+ // Yield before rrweb record() — the heaviest synchronous operation
300
+ await yieldToMain();
301
+ if (chunkSnapshot) {
302
+ // ── Chunked snapshot mode ──────────────────────────────────────
303
+ // Instead of rrweb's synchronous full-DOM snapshot (which blocks
304
+ // the main thread for 50-200ms+ on large pages), we:
305
+ // 1. Start rrweb with recordDOM:false (MutationObserver only)
306
+ // 2. Run our own async serializer with yield points
307
+ // 3. Emit Meta + FullSnapshot manually
308
+ // 4. Flush any mutations buffered during serialization
309
+ const { chunkedSnapshot } = await import("./chunkSerializer");
310
+ const mirror = record.mirror;
311
+ // Buffer mutations emitted by rrweb's MutationObserver during
312
+ // our async serialization, then flush after the FullSnapshot.
313
+ let isBuffering = true;
314
+ const mutationBuffer = [];
315
+ record({
316
+ emit(event) {
317
+ if (isBuffering) {
318
+ mutationBuffer.push(event);
319
+ }
320
+ else {
321
+ emitWithContext(event);
322
+ }
323
+ },
324
+ maskInputOptions: { text: true },
325
+ maskInputFn,
326
+ maskTextClass: MASK_CLASS,
327
+ ...captureSettings,
328
+ recordDOM: false, // after spread so it can't be overridden
329
+ });
330
+ const snapshotStartTime = Date.now();
331
+ const serializedDoc = await chunkedSnapshot(document, mirror, {
332
+ chunkSize: 500,
333
+ maxChunkMs: 16,
334
+ blockClass: captureSettings.blockClass,
335
+ blockSelector: captureSettings.blockSelector,
336
+ maskTextClass: captureSettings.maskTextClass ?? MASK_CLASS,
337
+ maskTextSelector: captureSettings.maskTextSelector,
338
+ });
339
+ if (serializedDoc) {
340
+ // Emit Meta event
341
+ emitWithContext({
342
+ type: EventType.Meta,
343
+ data: {
344
+ href: window.location.href,
345
+ width: document.documentElement.clientWidth ||
346
+ document.body.clientWidth,
347
+ height: document.documentElement.clientHeight ||
348
+ document.body.clientHeight,
349
+ },
350
+ timestamp: snapshotStartTime,
351
+ });
352
+ // Emit FullSnapshot event
353
+ emitWithContext({
354
+ type: EventType.FullSnapshot,
355
+ data: {
356
+ node: serializedDoc,
357
+ initialOffset: {
358
+ left: window.pageXOffset !== undefined
359
+ ? window.pageXOffset
360
+ : document.documentElement.scrollLeft,
361
+ top: window.pageYOffset !== undefined
362
+ ? window.pageYOffset
363
+ : document.documentElement.scrollTop,
364
+ },
365
+ },
366
+ timestamp: snapshotStartTime,
367
+ });
368
+ // Flush buffered mutations, then switch to direct emit
369
+ for (const event of mutationBuffer) {
370
+ emitWithContext(event);
371
+ }
372
+ isBuffering = false;
373
+ }
374
+ else {
375
+ // Serialization failed — continue without initial DOM snapshot.
376
+ // MutationObserver is still running, so ongoing changes are captured.
377
+ console.warn("[Sailfish] chunkSnapshot serialization failed; session continues without initial DOM snapshot");
378
+ isBuffering = false;
379
+ }
380
+ }
381
+ else {
382
+ // ── Normal snapshot mode ───────────────────────────────────────
383
+ record({
384
+ emit(event) {
385
+ emitWithContext(event);
386
+ },
387
+ maskInputOptions: { text: true },
388
+ maskInputFn,
389
+ maskTextClass: MASK_CLASS,
390
+ ...captureSettings,
391
+ });
392
+ }
284
393
  };
285
394
  if (deferRecordingStart) {
286
395
  // Deterministic deferral: start heavy work only after page load completes,
@@ -0,0 +1,9 @@
1
+ // Yields to the main thread to avoid long tasks (>50ms).
2
+ // Uses scheduler.yield() (Chrome 115+) with setTimeout(0) fallback.
3
+ export function yieldToMain() {
4
+ if (typeof globalThis !== "undefined" &&
5
+ globalThis.scheduler?.yield) {
6
+ return globalThis.scheduler.yield();
7
+ }
8
+ return new Promise((resolve) => setTimeout(resolve, 0));
9
+ }
@@ -0,0 +1,31 @@
1
+ /** Subset of rrweb's Mirror interface needed for registration */
2
+ export interface ChunkMirror {
3
+ add(node: Node, meta: {
4
+ id: number;
5
+ }): void;
6
+ hasNode(node: Node): boolean;
7
+ getId(node: Node): number;
8
+ }
9
+ export interface ChunkOptions {
10
+ /** Yield after this many nodes (default: 500) */
11
+ chunkSize?: number;
12
+ /** Yield if chunk exceeds this many ms (default: 16) */
13
+ maxChunkMs?: number;
14
+ /** rrweb blockClass — elements with this class are serialized as empty placeholders */
15
+ blockClass?: string;
16
+ /** rrweb blockSelector — CSS selector for blocked elements */
17
+ blockSelector?: string | null;
18
+ /** rrweb maskTextClass — text under elements with this class is masked */
19
+ maskTextClass?: string;
20
+ /** rrweb maskTextSelector — CSS selector for masked text containers */
21
+ maskTextSelector?: string | null;
22
+ }
23
+ /**
24
+ * Async chunked DOM serializer. Walks the document tree producing an
25
+ * rrweb-compatible serialized node tree, yielding to the main thread
26
+ * every `chunkSize` nodes or `maxChunkMs` milliseconds (whichever first).
27
+ *
28
+ * Nodes removed between yield points are silently skipped.
29
+ * Returns null if the document cannot be serialized.
30
+ */
31
+ export declare function chunkedSnapshot(doc: Document, mirror: ChunkMirror, options?: ChunkOptions): Promise<any | null>;
@@ -5,7 +5,7 @@ export declare const STORAGE_VERSION = 1;
5
5
  export declare const DEFAULT_CAPTURE_SETTINGS: CaptureSettings;
6
6
  export declare const DEFAULT_CONSOLE_RECORDING_SETTINGS: LogRecordOptions;
7
7
  export declare function matchUrlWithWildcard(input: unknown, patterns: string[]): boolean;
8
- export declare function startRecording({ apiKey, backendApi, domainsToPropagateHeaderTo, domainsToNotPropagateHeaderTo, serviceVersion, serviceIdentifier, gitSha, serviceAdditionalMetadata, enableIpTracking, captureStreamingResponseBody, captureResponseBodyMaxMb, captureStreamPrefixKb, captureStreamTimeoutMs, enableFiberTracking, deferRecordingStart, useWsWorker, }: {
8
+ export declare function startRecording({ apiKey, backendApi, domainsToPropagateHeaderTo, domainsToNotPropagateHeaderTo, serviceVersion, serviceIdentifier, gitSha, serviceAdditionalMetadata, enableIpTracking, captureStreamingResponseBody, captureResponseBodyMaxMb, captureStreamPrefixKb, captureStreamTimeoutMs, enableFiberTracking, deferRecording, deferRecordingStart, chunkSnapshot, useWsWorker, }: {
9
9
  apiKey: string;
10
10
  backendApi?: string;
11
11
  domainsToPropagateHeaderTo?: string[];
@@ -20,7 +20,10 @@ export declare function startRecording({ apiKey, backendApi, domainsToPropagateH
20
20
  captureResponseBodyMaxMb?: number;
21
21
  captureStreamPrefixKb?: number;
22
22
  captureStreamTimeoutMs?: number;
23
+ deferRecording?: boolean;
24
+ /** @deprecated Use `deferRecording` instead. */
23
25
  deferRecordingStart?: boolean;
26
+ chunkSnapshot?: boolean;
24
27
  useWsWorker?: boolean;
25
28
  }): Promise<void>;
26
29
  export declare const initRecorder: (options: {
@@ -40,7 +43,35 @@ export declare const initRecorder: (options: {
40
43
  captureResponseBodyMaxMb?: number;
41
44
  captureStreamPrefixKb?: number;
42
45
  captureStreamTimeoutMs?: number;
46
+ /**
47
+ * Defer heavy DOM recording (rrweb snapshot + MutationObserver) until after
48
+ * the page reaches TTI. Network interceptors (XHR/fetch), error handlers,
49
+ * and console capture still install immediately.
50
+ *
51
+ * When enabled, rrweb recording starts after page load completes AND one of:
52
+ * - `requestIdleCallback` fires (browser is genuinely idle)
53
+ * - First user interaction (click, scroll, keydown, touchstart)
54
+ * - 10-second hard ceiling timer
55
+ *
56
+ * Recommended for pages with heavy initial load (Module Federation, SPAs).
57
+ * @default true
58
+ */
59
+ deferRecording?: boolean;
60
+ /** @deprecated Use `deferRecording` instead. */
43
61
  deferRecordingStart?: boolean;
62
+ /**
63
+ * Break the initial DOM snapshot into chunks, yielding to the main thread
64
+ * between each chunk. Reduces the long-task duration of rrweb's initial
65
+ * full-DOM serialization on pages with large DOM trees (8 000+ nodes).
66
+ *
67
+ * Yields every 500 nodes or 16 ms, whichever comes first.
68
+ * Nodes removed between yield points are silently skipped.
69
+ *
70
+ * Best combined with `deferRecording: true` so the snapshot runs outside
71
+ * the Lighthouse measurement window entirely.
72
+ * @default false
73
+ */
74
+ chunkSnapshot?: boolean;
44
75
  useWsWorker?: boolean;
45
76
  }) => Promise<void>;
46
77
  export * from "./graphql";
@@ -1,5 +1,7 @@
1
1
  import type { LogRecordOptions } from "@sailfish-rrweb/rrweb-plugin-console-record";
2
2
  import type { WsHandle } from "./websocket";
3
+ /** Call after writing to sessionStorage keys used by getUrlAndStoredUuids(). */
4
+ export declare function invalidateUrlCache(): void;
3
5
  export declare const getUrlAndStoredUuids: () => {
4
6
  page_visit_uuid: string;
5
7
  prev_page_visit_uuid: string;
@@ -10,4 +12,4 @@ export declare const getUrlAndStoredUuids: () => {
10
12
  export declare function initializeDomContentEvents(sessionId: string): void;
11
13
  export declare function initializeConsolePlugin(consoleRecordSettings: LogRecordOptions, sessionId: string): Promise<void>;
12
14
  export declare function initializeRecording(captureSettings: any, // TODO - Sibyl post-launch - replace type
13
- backendApi: string, apiKey: string, sessionId: string, envValue?: string, deferRecordingStart?: boolean, useWsWorker?: boolean): Promise<WsHandle>;
15
+ backendApi: string, apiKey: string, sessionId: string, envValue?: string, deferRecordingStart?: boolean, useWsWorker?: boolean, chunkSnapshot?: boolean): Promise<WsHandle>;
@@ -0,0 +1 @@
1
+ export declare function yieldToMain(): Promise<void>;
@@ -3,6 +3,12 @@ export interface WsHandle {
3
3
  readyState: number;
4
4
  close: () => void;
5
5
  }
6
+ /** Install navigation listeners to keep the cached URL fresh. */
7
+ export declare function ensureHrefCache(): void;
8
+ /** Get cached href (falls back to live read if cache not initialized). */
9
+ export declare function getCachedHref(): string;
10
+ /** Get cached origin+pathname (no query/hash). */
11
+ export declare function getCachedHrefNoQuery(): string;
6
12
  /**
7
13
  * Clear stale function span tracking state (called from index.tsx to validate localStorage)
8
14
  * This disables tracking and clears localStorage if backend says tracking is not active
package/dist/utils.js CHANGED
@@ -37,7 +37,9 @@ export function buildBatches(queue, getSize, maxBytes) {
37
37
  return batches;
38
38
  }
39
39
  export function eventSize(event) {
40
- return new Blob([JSON.stringify(event)]).size;
40
+ // Fast byte-length estimate: JSON string length ≈ UTF-8 byte size for ASCII-heavy
41
+ // telemetry data. Avoids Blob constructor overhead. Accurate within ~5% for typical payloads.
42
+ return JSON.stringify(event).length;
41
43
  }
42
44
  // guard against old third party libraries which redefine Date.now
43
45
  let nowTimestamp = Date.now;
package/dist/websocket.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import ReconnectingWebSocket from "reconnecting-websocket";
2
2
  import { readDebugFlag } from "./env";
3
- import { deleteEventsByIds, getAllIndexedEvents, saveEventToIDB, } from "./eventStore";
3
+ import { deleteEventsByIds, getAllIndexedEvents, saveEventsToIDB, } from "./eventStore";
4
4
  import { deleteNotifyMessageById, getAllNotifyMessages, saveNotifyMessageToIDB, } from "./notifyEventStore";
5
5
  import { getOrSetSessionId } from "./session";
6
6
  import { buildBatches, eventSize } from "./utils";
@@ -96,6 +96,72 @@ let webSocket = null;
96
96
  let isDraining = false;
97
97
  let inFlightFlush = null;
98
98
  let flushIntervalId = null;
99
+ // ─── Cached window.location.href ──────────────────────────────────────────────
100
+ // Updated on navigation events (popstate, hashchange, pushState, replaceState)
101
+ // instead of reading the DOM property on every sendEvent() call.
102
+ let _cachedHref = "";
103
+ let _cachedHrefNoQuery = "";
104
+ let _hrefListenersInstalled = false;
105
+ function _updateHrefCache() {
106
+ _cachedHref = window.location.href;
107
+ _cachedHrefNoQuery = window.location.origin + window.location.pathname;
108
+ }
109
+ /** Install navigation listeners to keep the cached URL fresh. */
110
+ export function ensureHrefCache() {
111
+ if (_hrefListenersInstalled || typeof window === "undefined")
112
+ return;
113
+ _hrefListenersInstalled = true;
114
+ _updateHrefCache();
115
+ window.addEventListener("popstate", _updateHrefCache);
116
+ window.addEventListener("hashchange", _updateHrefCache);
117
+ const origPush = history.pushState;
118
+ history.pushState = function (...args) {
119
+ origPush.apply(this, args);
120
+ _updateHrefCache();
121
+ };
122
+ const origReplace = history.replaceState;
123
+ history.replaceState = function (...args) {
124
+ origReplace.apply(this, args);
125
+ _updateHrefCache();
126
+ };
127
+ }
128
+ /** Get cached href (falls back to live read if cache not initialized). */
129
+ export function getCachedHref() {
130
+ return (_cachedHref ||
131
+ (typeof window !== "undefined" ? window.location.href : ""));
132
+ }
133
+ /** Get cached origin+pathname (no query/hash). */
134
+ export function getCachedHrefNoQuery() {
135
+ return (_cachedHrefNoQuery ||
136
+ (typeof window !== "undefined"
137
+ ? window.location.origin + window.location.pathname
138
+ : ""));
139
+ }
140
+ // ─── IDB event batching ──────────────────────────────────────────────────────
141
+ // Queue events and flush in batches to avoid one IDB transaction per event.
142
+ const _idbEventQueue = [];
143
+ let _idbFlushTimer = null;
144
+ const IDB_BATCH_SIZE = 50;
145
+ const IDB_FLUSH_INTERVAL_MS = 100;
146
+ function queueEventForIDB(event) {
147
+ _idbEventQueue.push(event);
148
+ if (_idbEventQueue.length >= IDB_BATCH_SIZE) {
149
+ _flushIDBQueue();
150
+ }
151
+ else if (!_idbFlushTimer) {
152
+ _idbFlushTimer = setTimeout(_flushIDBQueue, IDB_FLUSH_INTERVAL_MS);
153
+ }
154
+ }
155
+ function _flushIDBQueue() {
156
+ if (_idbFlushTimer) {
157
+ clearTimeout(_idbFlushTimer);
158
+ _idbFlushTimer = null;
159
+ }
160
+ if (_idbEventQueue.length === 0)
161
+ return;
162
+ const batch = _idbEventQueue.splice(0);
163
+ saveEventsToIDB(batch);
164
+ }
99
165
  // Function span tracking state (only manages enabled/disabled)
100
166
  let funcSpanTrackingEnabled = false;
101
167
  let funcSpanTimeoutId = null;
@@ -325,13 +391,9 @@ export async function flushBufferedEvents() {
325
391
  if (!isWebSocketOpen(webSocket))
326
392
  break;
327
393
  const eventsToSend = batch.map((e) => {
328
- const event = {
329
- ...e.data,
330
- appUrl: e.data?.appUrl ?? window?.location?.href,
331
- };
332
- // Note: We do NOT add funcspan header to websocket events
333
- // The header is only added to HTTP network requests
334
- return event;
394
+ if (!e.data.appUrl)
395
+ e.data.appUrl = getCachedHref();
396
+ return e.data;
335
397
  });
336
398
  const idsToDelete = batch
337
399
  .map((e) => e.id)
@@ -359,23 +421,19 @@ export async function flushBufferedEvents() {
359
421
  }
360
422
  // ─── Public send API ─────────────────────────────────────────────────────────
361
423
  export function sendEvent(event) {
362
- const enrichedEvent = {
363
- ...event,
364
- app_url: event?.app_url ?? window?.location?.href,
365
- };
366
- // Note: We do NOT add funcspan header to websocket events
367
- // The header is only added to HTTP network requests (fetch/XMLHttpRequest)
368
- // This is handled in index.tsx injectHeader function
424
+ // Mutate in place — event is ephemeral, avoids object spread allocation
425
+ if (!event.app_url)
426
+ event.app_url = getCachedHref();
369
427
  if (isDraining || !isWebSocketOpen(webSocket)) {
370
- saveEventToIDB(enrichedEvent);
428
+ queueEventForIDB(event);
371
429
  return;
372
430
  }
373
431
  if (!wsSendPayload({
374
432
  type: "event",
375
- event: enrichedEvent,
433
+ event,
376
434
  mapUuid: window.sfMapUuid,
377
435
  })) {
378
- saveEventToIDB(enrichedEvent);
436
+ queueEventForIDB(event);
379
437
  }
380
438
  }
381
439
  // ─── WebSocket event handlers (shared between worker + direct paths) ─────────
@@ -542,6 +600,7 @@ function handleWsMessage(rawData) {
542
600
  }
543
601
  // ─── WebSocket initialization ────────────────────────────────────────────────
544
602
  export function initializeWebSocket(backendApi, apiKey, sessionId, envValue, useWsWorker = false) {
603
+ ensureHrefCache();
545
604
  const wsHost = getWebSocketHost(backendApi);
546
605
  const apiProtocol = new URL(backendApi).protocol;
547
606
  const wsScheme = apiProtocol === "https:" ? "wss" : "ws";
@@ -622,17 +681,14 @@ export function sendMessage(message) {
622
681
  if (!("sessionId" in message)) {
623
682
  message.sessionId = getOrSetSessionId();
624
683
  }
625
- const enriched = {
626
- ...message,
627
- app_url: message?.app_url ?? window?.location?.href,
628
- };
629
- // Check isDraining to prevent out-of-order delivery during reconnection
630
- // When draining buffered events, queue new messages to maintain order
684
+ // Mutate in place — avoids object spread allocation
685
+ if (!message.app_url)
686
+ message.app_url = getCachedHref();
631
687
  if (isDraining || !isWebSocketOpen(webSocket)) {
632
- saveNotifyMessageToIDB(JSON.stringify(enriched));
688
+ saveNotifyMessageToIDB(JSON.stringify(message));
633
689
  }
634
- else if (!wsSendPayload(enriched)) {
635
- saveNotifyMessageToIDB(JSON.stringify(enriched));
690
+ else if (!wsSendPayload(message)) {
691
+ saveNotifyMessageToIDB(JSON.stringify(message));
636
692
  }
637
693
  }
638
694
  function getWebSocketHost(url) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailfish-ai/recorder",
3
- "version": "1.10.5",
3
+ "version": "1.10.7",
4
4
  "publishPublicly": true,
5
5
  "type": "module",
6
6
  "main": "dist/recorder.cjs",