@usero/sdk 0.3.1 → 0.3.4

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.
@@ -19,6 +19,8 @@ interface FeedbackSubmission {
19
19
  screenshots?: ScreenshotData[];
20
20
  metadata?: Record<string, unknown>;
21
21
  replayEvents?: string;
22
+ sessionReplayId?: string;
23
+ replayOffsetMs?: number;
22
24
  }
23
25
 
24
26
  interface PluginLogger {
@@ -48,7 +50,6 @@ interface ReplaySampling {
48
50
  input?: number | 'last';
49
51
  }
50
52
  interface SessionReplayOptions {
51
- bufferSeconds?: number;
52
53
  startAfterMs?: number;
53
54
  sampleRate?: number;
54
55
  sampling?: ReplaySampling;
@@ -56,20 +57,41 @@ interface SessionReplayOptions {
56
57
  maskTextSelector?: string;
57
58
  inlineStylesheet?: boolean;
58
59
  blockSelector?: string;
60
+ chunkSeconds?: number;
61
+ chunkMaxEvents?: number;
62
+ chunkMaxAttempts?: number;
63
+ apiUrl?: string;
59
64
  }
60
- interface RrwebEvent {
61
- type: number;
62
- data: unknown;
63
- timestamp: number;
64
- }
65
- declare function evictOldEvents(events: RrwebEvent[], bufferSeconds: number, now: number): void;
66
65
  declare function uint8ToBase64(bytes: Uint8Array): string;
67
- declare function gzipString(input: string): Promise<string>;
66
+ declare function gzipBytes(input: string): Promise<Uint8Array>;
67
+ declare function mintSdkSessionId(): string;
68
+ declare function joinUrl(apiUrl: string, path: string): string;
69
+ interface CreateSessionResult {
70
+ accepted: boolean;
71
+ sessionReplayId?: string;
72
+ dropReason?: string;
73
+ }
74
+ declare function createSession(apiUrl: string, clientId: string, sdkSessionId: string): Promise<CreateSessionResult | null>;
75
+ interface ChunkUploadResult {
76
+ ok: boolean;
77
+ stopSession: boolean;
78
+ }
79
+ declare function uploadChunk(apiUrl: string, sessionReplayId: string, clientId: string, seq: number, bytes: Uint8Array, eventCount: number, durationMs: number, logger: PluginContext['logger'], maxAttempts: number): Promise<ChunkUploadResult>;
80
+ interface CurrentSessionHandle {
81
+ id: string;
82
+ offsetMs: number;
83
+ }
68
84
  declare function sessionReplay(options?: SessionReplayOptions): UseroPlugin;
85
+ declare function getCurrentSession(ctx: PluginContext): CurrentSessionHandle | null;
69
86
  declare const __test__: {
70
- evictOldEvents: typeof evictOldEvents;
71
- gzipString: typeof gzipString;
72
87
  uint8ToBase64: typeof uint8ToBase64;
88
+ gzipBytes: typeof gzipBytes;
89
+ mintSdkSessionId: typeof mintSdkSessionId;
90
+ uploadChunk: typeof uploadChunk;
91
+ createSession: typeof createSession;
92
+ joinUrl: typeof joinUrl;
93
+ HARD_CHUNK_BYTE_CAP: number;
94
+ SDK_SESSION_STORAGE_KEY: string;
73
95
  };
74
96
 
75
- export { type ReplaySampling, type SessionReplayOptions, __test__, sessionReplay };
97
+ export { type CurrentSessionHandle, type ReplaySampling, type SessionReplayOptions, __test__, getCurrentSession, sessionReplay };
@@ -19,6 +19,8 @@ interface FeedbackSubmission {
19
19
  screenshots?: ScreenshotData[];
20
20
  metadata?: Record<string, unknown>;
21
21
  replayEvents?: string;
22
+ sessionReplayId?: string;
23
+ replayOffsetMs?: number;
22
24
  }
23
25
 
24
26
  interface PluginLogger {
@@ -48,7 +50,6 @@ interface ReplaySampling {
48
50
  input?: number | 'last';
49
51
  }
50
52
  interface SessionReplayOptions {
51
- bufferSeconds?: number;
52
53
  startAfterMs?: number;
53
54
  sampleRate?: number;
54
55
  sampling?: ReplaySampling;
@@ -56,20 +57,41 @@ interface SessionReplayOptions {
56
57
  maskTextSelector?: string;
57
58
  inlineStylesheet?: boolean;
58
59
  blockSelector?: string;
60
+ chunkSeconds?: number;
61
+ chunkMaxEvents?: number;
62
+ chunkMaxAttempts?: number;
63
+ apiUrl?: string;
59
64
  }
60
- interface RrwebEvent {
61
- type: number;
62
- data: unknown;
63
- timestamp: number;
64
- }
65
- declare function evictOldEvents(events: RrwebEvent[], bufferSeconds: number, now: number): void;
66
65
  declare function uint8ToBase64(bytes: Uint8Array): string;
67
- declare function gzipString(input: string): Promise<string>;
66
+ declare function gzipBytes(input: string): Promise<Uint8Array>;
67
+ declare function mintSdkSessionId(): string;
68
+ declare function joinUrl(apiUrl: string, path: string): string;
69
+ interface CreateSessionResult {
70
+ accepted: boolean;
71
+ sessionReplayId?: string;
72
+ dropReason?: string;
73
+ }
74
+ declare function createSession(apiUrl: string, clientId: string, sdkSessionId: string): Promise<CreateSessionResult | null>;
75
+ interface ChunkUploadResult {
76
+ ok: boolean;
77
+ stopSession: boolean;
78
+ }
79
+ declare function uploadChunk(apiUrl: string, sessionReplayId: string, clientId: string, seq: number, bytes: Uint8Array, eventCount: number, durationMs: number, logger: PluginContext['logger'], maxAttempts: number): Promise<ChunkUploadResult>;
80
+ interface CurrentSessionHandle {
81
+ id: string;
82
+ offsetMs: number;
83
+ }
68
84
  declare function sessionReplay(options?: SessionReplayOptions): UseroPlugin;
85
+ declare function getCurrentSession(ctx: PluginContext): CurrentSessionHandle | null;
69
86
  declare const __test__: {
70
- evictOldEvents: typeof evictOldEvents;
71
- gzipString: typeof gzipString;
72
87
  uint8ToBase64: typeof uint8ToBase64;
88
+ gzipBytes: typeof gzipBytes;
89
+ mintSdkSessionId: typeof mintSdkSessionId;
90
+ uploadChunk: typeof uploadChunk;
91
+ createSession: typeof createSession;
92
+ joinUrl: typeof joinUrl;
93
+ HARD_CHUNK_BYTE_CAP: number;
94
+ SDK_SESSION_STORAGE_KEY: string;
73
95
  };
74
96
 
75
- export { type ReplaySampling, type SessionReplayOptions, __test__, sessionReplay };
97
+ export { type CurrentSessionHandle, type ReplaySampling, type SessionReplayOptions, __test__, getCurrentSession, sessionReplay };
@@ -1,41 +1,133 @@
1
1
  // src/plugins/session-replay.ts
2
- var DEFAULT_OPTIONS = {
3
- bufferSeconds: 30,
2
+ var DEFAULTS = {
4
3
  startAfterMs: 0,
5
4
  sampleRate: 1,
6
5
  sampling: { mousemove: 50, scroll: 100 },
7
6
  maskAllInputs: true,
8
7
  maskTextSelector: "[data-usero-mask]",
9
8
  inlineStylesheet: true,
10
- blockSelector: "[data-usero-block]"
9
+ blockSelector: "[data-usero-block]",
10
+ chunkSeconds: 10,
11
+ chunkMaxEvents: 5e3,
12
+ chunkMaxAttempts: 5,
13
+ apiUrl: ""
11
14
  };
12
- function evictOldEvents(events, bufferSeconds, now) {
13
- if (events.length === 0) return;
14
- const cutoff = now - bufferSeconds * 1e3;
15
- let dropCount = 0;
16
- for (const e of events) {
17
- if (e.timestamp >= cutoff) break;
18
- dropCount++;
19
- }
20
- if (dropCount > 0) events.splice(0, dropCount);
21
- }
15
+ var SDK_SESSION_STORAGE_KEY = "usero:session-replay:sdk-session-id";
16
+ var HARD_CHUNK_BYTE_CAP = 4 * 1024 * 1024;
22
17
  function uint8ToBase64(bytes) {
23
18
  let binary = "";
24
19
  const chunkSize = 32768;
25
20
  for (let i = 0; i < bytes.length; i += chunkSize) {
26
- const chunk = bytes.subarray(i, i + chunkSize);
27
- binary += String.fromCharCode.apply(null, Array.from(chunk));
21
+ const slice = bytes.subarray(i, i + chunkSize);
22
+ binary += String.fromCharCode.apply(null, Array.from(slice));
28
23
  }
29
24
  return typeof btoa === "function" ? btoa(binary) : "";
30
25
  }
31
- async function gzipString(input) {
26
+ async function gzipBytes(input) {
32
27
  if (typeof CompressionStream === "undefined") {
33
- const bytes = new TextEncoder().encode(input);
34
- return uint8ToBase64(bytes);
28
+ return new TextEncoder().encode(input);
35
29
  }
36
30
  const stream = new Blob([input]).stream().pipeThrough(new CompressionStream("gzip"));
37
- const compressed = await new Response(stream).arrayBuffer();
38
- return uint8ToBase64(new Uint8Array(compressed));
31
+ const buf = await new Response(stream).arrayBuffer();
32
+ return new Uint8Array(buf);
33
+ }
34
+ function generateRandomId() {
35
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
36
+ return crypto.randomUUID();
37
+ }
38
+ const bytes = new Uint8Array(16);
39
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
40
+ crypto.getRandomValues(bytes);
41
+ } else {
42
+ for (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256);
43
+ }
44
+ let out = "";
45
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
46
+ return out;
47
+ }
48
+ function mintSdkSessionId() {
49
+ try {
50
+ const existing = window.sessionStorage?.getItem(SDK_SESSION_STORAGE_KEY);
51
+ if (existing && /^[a-z0-9-]{8,}$/i.test(existing)) return existing;
52
+ } catch {
53
+ }
54
+ const id = generateRandomId();
55
+ try {
56
+ window.sessionStorage?.setItem(SDK_SESSION_STORAGE_KEY, id);
57
+ } catch {
58
+ }
59
+ return id;
60
+ }
61
+ function joinUrl(apiUrl, path) {
62
+ return `${apiUrl.replace(/\/$/, "")}${path}`;
63
+ }
64
+ async function createSession(apiUrl, clientId, sdkSessionId) {
65
+ try {
66
+ const startUrl = typeof window !== "undefined" && window.location ? window.location.href : void 0;
67
+ const userAgent = typeof navigator !== "undefined" && navigator.userAgent ? navigator.userAgent : void 0;
68
+ const res = await fetch(joinUrl(apiUrl, "/api/replay-sessions"), {
69
+ method: "POST",
70
+ headers: { "Content-Type": "application/json" },
71
+ body: JSON.stringify({
72
+ clientId,
73
+ sdkSessionId,
74
+ startUrl,
75
+ userAgent,
76
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
77
+ })
78
+ });
79
+ if (!res.ok) return null;
80
+ const json = await res.json();
81
+ if (typeof json.accepted !== "boolean") return null;
82
+ const result = { accepted: json.accepted };
83
+ if (typeof json.sessionReplayId === "string") result.sessionReplayId = json.sessionReplayId;
84
+ if (typeof json.dropReason === "string") result.dropReason = json.dropReason;
85
+ return result;
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+ async function uploadChunk(apiUrl, sessionReplayId, clientId, seq, bytes, eventCount, durationMs, logger, maxAttempts) {
91
+ const url = joinUrl(
92
+ apiUrl,
93
+ `/api/replay-sessions/${encodeURIComponent(sessionReplayId)}/chunks/${seq}`
94
+ );
95
+ let attempt = 0;
96
+ while (attempt < maxAttempts) {
97
+ try {
98
+ const buffer = bytes.buffer.slice(
99
+ bytes.byteOffset,
100
+ bytes.byteOffset + bytes.byteLength
101
+ );
102
+ const blob = new Blob([buffer], { type: "application/octet-stream" });
103
+ const res = await fetch(url, {
104
+ method: "PUT",
105
+ body: blob,
106
+ headers: {
107
+ "Content-Type": "application/octet-stream",
108
+ "X-Usero-Client-Id": clientId,
109
+ "X-Usero-Event-Count": String(eventCount),
110
+ "X-Usero-Duration-Ms": String(Math.max(0, Math.round(durationMs)))
111
+ }
112
+ });
113
+ if (res.ok) return { ok: true, stopSession: false };
114
+ if (res.status === 409) {
115
+ logger.warn(`chunk ${seq} rejected with 409, stopping session`);
116
+ return { ok: false, stopSession: true };
117
+ }
118
+ if (res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429) {
119
+ logger.error(`chunk ${seq} rejected with ${res.status}`);
120
+ return { ok: false, stopSession: false };
121
+ }
122
+ } catch (err) {
123
+ logger.warn(`chunk ${seq} attempt ${attempt + 1} failed`, err);
124
+ }
125
+ attempt += 1;
126
+ const backoff = Math.min(15e3, 500 * 2 ** attempt) + Math.floor(Math.random() * 250);
127
+ await new Promise((resolve) => setTimeout(resolve, backoff));
128
+ }
129
+ logger.error(`chunk ${seq} dropped after ${maxAttempts} attempts`);
130
+ return { ok: false, stopSession: false };
39
131
  }
40
132
  async function loadRrwebRecord() {
41
133
  try {
@@ -51,20 +143,95 @@ async function loadRrwebRecord() {
51
143
  return null;
52
144
  }
53
145
  }
146
+ function scheduleChunkUpload(store, ctx) {
147
+ if (!store.sessionReplayId) return;
148
+ if (store.pendingEvents.length === 0) return;
149
+ const events = store.pendingEvents;
150
+ const eventCount = events.length;
151
+ const firstTs = store.pendingFirstTs ?? 0;
152
+ const lastTs = store.pendingLastTs ?? firstTs;
153
+ const durationMs = Math.max(0, lastTs - firstTs);
154
+ const seq = store.nextChunkSeq;
155
+ store.nextChunkSeq += 1;
156
+ store.pendingEvents = [];
157
+ store.pendingFirstTs = null;
158
+ store.pendingLastTs = null;
159
+ const sessionReplayId = store.sessionReplayId;
160
+ const apiUrl = store.options.apiUrl;
161
+ const clientId = store.clientId;
162
+ const maxAttempts = store.options.chunkMaxAttempts;
163
+ store.pendingUploads += 1;
164
+ store.uploadQueue = store.uploadQueue.then(async () => {
165
+ try {
166
+ if (store.cancelled) return;
167
+ const json = JSON.stringify(events);
168
+ const bytes = await gzipBytes(json);
169
+ if (bytes.byteLength > HARD_CHUNK_BYTE_CAP) {
170
+ ctx.logger.error(
171
+ `chunk ${seq} exceeds 4MB hard cap (${bytes.byteLength} bytes), dropping`
172
+ );
173
+ return;
174
+ }
175
+ const result = await uploadChunk(
176
+ apiUrl,
177
+ sessionReplayId,
178
+ clientId,
179
+ seq,
180
+ bytes,
181
+ eventCount,
182
+ durationMs,
183
+ ctx.logger,
184
+ maxAttempts
185
+ );
186
+ if (result.stopSession) {
187
+ store.stopped = true;
188
+ stopRrweb(store);
189
+ }
190
+ } catch (err) {
191
+ ctx.logger.error(`chunk ${seq} encode failed`, err);
192
+ } finally {
193
+ store.pendingUploads -= 1;
194
+ }
195
+ });
196
+ }
197
+ function flushPendingChunk(store, ctx) {
198
+ if (store.stopped || store.cancelled) return;
199
+ if (store.pendingEvents.length === 0) return;
200
+ scheduleChunkUpload(store, ctx);
201
+ }
202
+ function stopRrweb(store) {
203
+ if (store.stopRecording) {
204
+ try {
205
+ store.stopRecording();
206
+ } catch {
207
+ }
208
+ store.stopRecording = null;
209
+ }
210
+ if (store.chunkFlushTimer) {
211
+ clearInterval(store.chunkFlushTimer);
212
+ store.chunkFlushTimer = null;
213
+ }
214
+ }
54
215
  function startRecording(store, ctx) {
55
- if (store.cancelled || store.stopRecording || store.loadInProgress) return;
216
+ if (store.cancelled || store.stopped || store.stopRecording || store.loadInProgress) return;
56
217
  store.loadInProgress = true;
57
218
  void loadRrwebRecord().then((record) => {
58
219
  store.loadInProgress = false;
59
- if (store.cancelled || !record) {
220
+ if (store.cancelled || store.stopped || !record) {
60
221
  if (!record) ctx.logger.warn("rrweb failed to load, replay disabled");
61
222
  return;
62
223
  }
63
224
  try {
64
225
  const stop = record({
65
226
  emit: (event) => {
66
- store.events.push(event);
67
- evictOldEvents(store.events, store.options.bufferSeconds, event.timestamp);
227
+ if (store.stopped || store.cancelled) return;
228
+ if (store.recordingStartedAt === null) store.recordingStartedAt = event.timestamp;
229
+ store.pendingEvents.push(event);
230
+ if (store.pendingFirstTs === null) store.pendingFirstTs = event.timestamp;
231
+ store.pendingLastTs = event.timestamp;
232
+ if (store.pendingEvents.length >= store.options.chunkMaxEvents) {
233
+ scheduleChunkUpload(store, ctx);
234
+ }
68
235
  },
69
236
  maskAllInputs: store.options.maskAllInputs,
70
237
  maskTextSelector: store.options.maskTextSelector || void 0,
@@ -73,45 +240,124 @@ function startRecording(store, ctx) {
73
240
  sampling: store.options.sampling
74
241
  });
75
242
  store.stopRecording = stop;
243
+ store.record = record;
244
+ scheduleShadowSnapshot(store, ctx);
245
+ store.chunkFlushTimer = setInterval(
246
+ () => flushPendingChunk(store, ctx),
247
+ store.options.chunkSeconds * 1e3
248
+ );
76
249
  } catch (err) {
77
250
  ctx.logger.error("rrweb record() threw", err);
78
251
  }
79
252
  });
80
253
  }
254
+ function scheduleShadowSnapshot(store, ctx) {
255
+ if (store.cancelled || store.stopped || !store.record || !store.stopRecording) return;
256
+ const fn = store.record.takeFullSnapshot;
257
+ if (typeof fn !== "function") return;
258
+ try {
259
+ fn(true);
260
+ } catch (err) {
261
+ ctx.logger.warn("takeFullSnapshot threw", err);
262
+ }
263
+ }
264
+ function finalise(store, ctx, opts) {
265
+ if (!store.sessionReplayId) return;
266
+ if (store.pendingEvents.length > 0) flushPendingChunk(store, ctx);
267
+ const url = joinUrl(
268
+ store.options.apiUrl,
269
+ `/api/replay-sessions/${encodeURIComponent(store.sessionReplayId)}/finalise`
270
+ );
271
+ const body = JSON.stringify({ clientId: store.clientId, endedAt: (/* @__PURE__ */ new Date()).toISOString() });
272
+ if (opts.useBeacon && typeof navigator !== "undefined" && navigator.sendBeacon) {
273
+ try {
274
+ const blob = new Blob([body], { type: "application/json" });
275
+ navigator.sendBeacon(url, blob);
276
+ return;
277
+ } catch (err) {
278
+ ctx.logger.warn("finalise sendBeacon threw", err);
279
+ }
280
+ }
281
+ void fetch(url, {
282
+ method: "POST",
283
+ body,
284
+ headers: { "Content-Type": "application/json" },
285
+ keepalive: true
286
+ }).catch((err) => ctx.logger.warn("finalise fetch failed", err));
287
+ }
81
288
  function sessionReplay(options = {}) {
82
289
  const merged = {
83
- ...DEFAULT_OPTIONS,
290
+ ...DEFAULTS,
84
291
  ...options,
85
- sampling: { ...DEFAULT_OPTIONS.sampling, ...options.sampling ?? {} }
292
+ sampling: { ...DEFAULTS.sampling, ...options.sampling ?? {} }
86
293
  };
87
294
  return {
88
295
  name: "session-replay",
89
296
  onInit(ctx) {
297
+ if (typeof window === "undefined") return;
90
298
  if (merged.sampleRate < 1 && Math.random() >= merged.sampleRate) {
91
299
  ctx.logger.debug("skipped by sampleRate");
92
300
  return;
93
301
  }
94
- if (typeof window === "undefined") return;
302
+ const apiUrl = merged.apiUrl || ctx.baseUrl;
303
+ if (!apiUrl) {
304
+ ctx.logger.error("session-replay needs an apiUrl (via options or PluginContext)");
305
+ return;
306
+ }
307
+ const sdkSessionId = mintSdkSessionId();
95
308
  const store = {
96
- events: [],
97
- stopRecording: null,
309
+ options: { ...merged, apiUrl },
310
+ clientId: ctx.clientId,
311
+ sdkSessionId,
312
+ sessionReplayId: null,
313
+ recordingStartedAt: null,
314
+ pendingEvents: [],
315
+ pendingFirstTs: null,
316
+ pendingLastTs: null,
317
+ nextChunkSeq: 0,
318
+ uploadQueue: Promise.resolve(),
319
+ pendingUploads: 0,
320
+ chunkFlushTimer: null,
98
321
  startTimer: null,
99
322
  pageHideHandler: null,
323
+ shadowUpdateHandler: null,
324
+ record: null,
325
+ stopRecording: null,
100
326
  loadInProgress: false,
101
327
  cancelled: false,
102
- options: merged
328
+ stopped: false
103
329
  };
104
330
  ctx.setStore(store);
105
- const begin = () => {
106
- if (store.startTimer) {
107
- clearTimeout(store.startTimer);
108
- store.startTimer = null;
331
+ const onShadowUpdate = () => scheduleShadowSnapshot(store, ctx);
332
+ store.shadowUpdateHandler = onShadowUpdate;
333
+ window.addEventListener("usero:shadow-update", onShadowUpdate);
334
+ const onPageHide = () => {
335
+ finalise(store, ctx, { useBeacon: true });
336
+ store.stopped = true;
337
+ stopRrweb(store);
338
+ };
339
+ store.pageHideHandler = onPageHide;
340
+ window.addEventListener("pagehide", onPageHide);
341
+ const begin = async () => {
342
+ if (store.cancelled) return;
343
+ const created = await createSession(apiUrl, ctx.clientId, sdkSessionId);
344
+ if (!created) {
345
+ ctx.logger.warn("session create failed, replay disabled");
346
+ store.stopped = true;
347
+ return;
109
348
  }
110
- if (store.pageHideHandler) {
111
- window.removeEventListener("pagehide", store.pageHideHandler);
112
- window.removeEventListener("beforeunload", store.pageHideHandler);
113
- store.pageHideHandler = null;
349
+ if (!created.accepted) {
350
+ ctx.logger.info(`session-replay declined: ${created.dropReason ?? "unknown"}`);
351
+ store.stopped = true;
352
+ return;
114
353
  }
354
+ if (!created.sessionReplayId) {
355
+ ctx.logger.error("server accepted but returned no sessionReplayId");
356
+ store.stopped = true;
357
+ return;
358
+ }
359
+ store.sessionReplayId = created.sessionReplayId;
360
+ store.recordingStartedAt = Date.now();
115
361
  startRecording(store, ctx);
116
362
  };
117
363
  if (merged.startAfterMs > 0) {
@@ -121,55 +367,65 @@ function sessionReplay(options = {}) {
121
367
  clearTimeout(store.startTimer);
122
368
  store.startTimer = null;
123
369
  }
124
- if (store.pageHideHandler) {
125
- window.removeEventListener("pagehide", store.pageHideHandler);
126
- window.removeEventListener("beforeunload", store.pageHideHandler);
127
- store.pageHideHandler = null;
128
- }
129
370
  };
130
- store.pageHideHandler = cancelOnExit;
131
371
  window.addEventListener("pagehide", cancelOnExit, { once: true });
132
372
  window.addEventListener("beforeunload", cancelOnExit, { once: true });
133
- store.startTimer = setTimeout(begin, merged.startAfterMs);
373
+ store.startTimer = setTimeout(() => {
374
+ void begin();
375
+ }, merged.startAfterMs);
134
376
  } else {
135
- begin();
377
+ void begin();
136
378
  }
137
379
  },
138
- async onFeedbackSubmit(ctx) {
380
+ onFeedbackSubmit(ctx) {
139
381
  const store = ctx.getStore();
140
- if (!store || store.cancelled || store.events.length === 0) return void 0;
141
- const snapshot = store.events.slice();
142
- try {
143
- const json = JSON.stringify(snapshot);
144
- const replayEvents = await gzipString(json);
145
- return { replayEvents };
146
- } catch (err) {
147
- ctx.logger.error("failed to encode replay buffer", err);
148
- return void 0;
149
- }
382
+ if (!store || store.cancelled || store.stopped) return void 0;
383
+ if (!store.sessionReplayId) return void 0;
384
+ const offsetMs = store.recordingStartedAt !== null ? Math.max(0, Date.now() - store.recordingStartedAt) : 0;
385
+ return { sessionReplayId: store.sessionReplayId, replayOffsetMs: offsetMs };
150
386
  },
151
387
  onDestroy(ctx) {
152
388
  const store = ctx.getStore();
153
389
  if (!store) return;
154
390
  store.cancelled = true;
155
- if (store.startTimer) clearTimeout(store.startTimer);
391
+ if (store.startTimer) {
392
+ clearTimeout(store.startTimer);
393
+ store.startTimer = null;
394
+ }
156
395
  if (store.pageHideHandler) {
157
396
  window.removeEventListener("pagehide", store.pageHideHandler);
158
- window.removeEventListener("beforeunload", store.pageHideHandler);
397
+ store.pageHideHandler = null;
159
398
  }
160
- if (store.stopRecording) {
161
- try {
162
- store.stopRecording();
163
- } catch (err) {
164
- ctx.logger.warn("rrweb stop threw", err);
165
- }
399
+ if (store.shadowUpdateHandler) {
400
+ window.removeEventListener("usero:shadow-update", store.shadowUpdateHandler);
401
+ store.shadowUpdateHandler = null;
402
+ }
403
+ if (store.sessionReplayId && !store.stopped) {
404
+ finalise(store, ctx, { useBeacon: false });
166
405
  }
167
- store.events.length = 0;
406
+ store.stopped = true;
407
+ stopRrweb(store);
408
+ store.pendingEvents.length = 0;
168
409
  }
169
410
  };
170
411
  }
171
- var __test__ = { evictOldEvents, gzipString, uint8ToBase64 };
412
+ function getCurrentSession(ctx) {
413
+ const store = ctx.getStore();
414
+ if (!store || store.cancelled || store.stopped || !store.sessionReplayId) return null;
415
+ const offsetMs = store.recordingStartedAt !== null ? Math.max(0, Date.now() - store.recordingStartedAt) : 0;
416
+ return { id: store.sessionReplayId, offsetMs };
417
+ }
418
+ var __test__ = {
419
+ uint8ToBase64,
420
+ gzipBytes,
421
+ mintSdkSessionId,
422
+ uploadChunk,
423
+ createSession,
424
+ joinUrl,
425
+ HARD_CHUNK_BYTE_CAP,
426
+ SDK_SESSION_STORAGE_KEY
427
+ };
172
428
 
173
- export { __test__, sessionReplay };
429
+ export { __test__, getCurrentSession, sessionReplay };
174
430
  //# sourceMappingURL=session-replay.js.map
175
431
  //# sourceMappingURL=session-replay.js.map