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