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