@cedarai/session-replay-sdk 0.1.0 → 0.2.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/index.js +104 -12
- package/package.json +1 -2
- package/dist/global.global.js +0 -4076
- package/src/console.test.ts +0 -148
- package/src/console.ts +0 -54
- package/src/errors.test.ts +0 -146
- package/src/errors.ts +0 -99
- package/src/global.ts +0 -3
- package/src/index.test.ts +0 -207
- package/src/index.ts +0 -122
- package/src/network.test.ts +0 -135
- package/src/network.ts +0 -112
- package/src/recorder.test.ts +0 -187
- package/src/recorder.ts +0 -47
- package/src/transport.test.ts +0 -256
- package/src/transport.ts +0 -114
- package/src/types.ts +0 -101
- package/tsconfig.json +0 -9
- package/vitest.config.ts +0 -9
package/dist/index.js
CHANGED
|
@@ -1,4 +1,29 @@
|
|
|
1
|
+
// src/compress.ts
|
|
2
|
+
async function gzipCompress(data) {
|
|
3
|
+
const blob = new Blob([data]);
|
|
4
|
+
const stream = blob.stream().pipeThrough(new CompressionStream("gzip"));
|
|
5
|
+
const reader = stream.getReader();
|
|
6
|
+
const chunks = [];
|
|
7
|
+
for (; ; ) {
|
|
8
|
+
const { done, value } = await reader.read();
|
|
9
|
+
if (done) break;
|
|
10
|
+
chunks.push(value);
|
|
11
|
+
}
|
|
12
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
13
|
+
const result = new Uint8Array(totalLength);
|
|
14
|
+
let offset = 0;
|
|
15
|
+
for (const chunk of chunks) {
|
|
16
|
+
result.set(chunk, offset);
|
|
17
|
+
offset += chunk.length;
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
function isCompressionSupported() {
|
|
22
|
+
return typeof CompressionStream !== "undefined";
|
|
23
|
+
}
|
|
24
|
+
|
|
1
25
|
// src/transport.ts
|
|
26
|
+
var BEACON_MAX_SIZE = 64 * 1024;
|
|
2
27
|
var Transport = class {
|
|
3
28
|
config;
|
|
4
29
|
queue = [];
|
|
@@ -6,6 +31,9 @@ var Transport = class {
|
|
|
6
31
|
started = false;
|
|
7
32
|
startedAt = Date.now();
|
|
8
33
|
fetchFn;
|
|
34
|
+
// Pre-compressed beacon payload, updated in the background after each enqueue
|
|
35
|
+
pendingBeaconBlob = null;
|
|
36
|
+
pendingBeaconUrl = "";
|
|
9
37
|
/**
|
|
10
38
|
* @param config SDK configuration
|
|
11
39
|
* @param originalFetch Original fetch reference to avoid self-interception.
|
|
@@ -42,31 +70,88 @@ var Transport = class {
|
|
|
42
70
|
if (estimatedSize >= maxSize) {
|
|
43
71
|
this.flush();
|
|
44
72
|
}
|
|
73
|
+
this.prepareBeaconPayload();
|
|
45
74
|
}
|
|
46
75
|
flush() {
|
|
47
76
|
if (this.queue.length === 0) return;
|
|
48
77
|
const events = this.queue.splice(0);
|
|
49
78
|
const payload = this.buildPayload(events);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
79
|
+
const json = JSON.stringify(payload);
|
|
80
|
+
this.pendingBeaconBlob = null;
|
|
81
|
+
if (isCompressionSupported()) {
|
|
82
|
+
gzipCompress(json).then((compressed) => {
|
|
83
|
+
this.fetchFn(`${this.config.serverUrl}/api/ingest`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: {
|
|
86
|
+
"Content-Type": "application/json",
|
|
87
|
+
"Content-Encoding": "gzip"
|
|
88
|
+
},
|
|
89
|
+
body: compressed
|
|
90
|
+
}).catch(() => {
|
|
91
|
+
});
|
|
92
|
+
}).catch(() => {
|
|
93
|
+
this.fetchFn(`${this.config.serverUrl}/api/ingest`, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: { "Content-Type": "application/json" },
|
|
96
|
+
body: json
|
|
97
|
+
}).catch(() => {
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
} else {
|
|
101
|
+
this.fetchFn(`${this.config.serverUrl}/api/ingest`, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: { "Content-Type": "application/json" },
|
|
104
|
+
body: json
|
|
105
|
+
}).catch(() => {
|
|
106
|
+
});
|
|
107
|
+
}
|
|
56
108
|
}
|
|
57
109
|
/**
|
|
58
|
-
* Flush via sendBeacon for page unload.
|
|
59
|
-
*
|
|
110
|
+
* Flush via sendBeacon for page unload. Uses pre-compressed data if available,
|
|
111
|
+
* otherwise sends uncompressed JSON with truncation to stay under 64KB.
|
|
60
112
|
*/
|
|
61
113
|
flushOnUnload() {
|
|
62
114
|
if (this.queue.length === 0) return;
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
115
|
+
const url = `${this.config.serverUrl}/api/ingest`;
|
|
116
|
+
if (this.pendingBeaconBlob) {
|
|
117
|
+
navigator.sendBeacon(this.pendingBeaconUrl, this.pendingBeaconBlob);
|
|
118
|
+
this.queue.length = 0;
|
|
119
|
+
this.pendingBeaconBlob = null;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
let events = this.queue.splice(0);
|
|
123
|
+
let payload = this.buildPayload(events);
|
|
124
|
+
let body = JSON.stringify(payload);
|
|
125
|
+
while (body.length > BEACON_MAX_SIZE && events.length > 1) {
|
|
126
|
+
const rrwebIdx = events.findLastIndex((e) => e.type === "rrweb");
|
|
127
|
+
if (rrwebIdx >= 0) {
|
|
128
|
+
events.splice(rrwebIdx, 1);
|
|
129
|
+
} else {
|
|
130
|
+
events.pop();
|
|
131
|
+
}
|
|
132
|
+
payload = this.buildPayload(events);
|
|
133
|
+
body = JSON.stringify(payload);
|
|
134
|
+
}
|
|
66
135
|
const blob = new Blob([body], { type: "application/json" });
|
|
67
|
-
const url = `${this.config.serverUrl}/api/ingest?encoding=gzip`;
|
|
68
136
|
navigator.sendBeacon(url, blob);
|
|
69
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Pre-compress the current queue in the background for sendBeacon use.
|
|
140
|
+
* Called after each enqueue so the compressed blob is ready when beforeunload fires.
|
|
141
|
+
*/
|
|
142
|
+
prepareBeaconPayload() {
|
|
143
|
+
if (!isCompressionSupported() || this.queue.length === 0) return;
|
|
144
|
+
const snapshot = [...this.queue];
|
|
145
|
+
const payload = this.buildPayload(snapshot);
|
|
146
|
+
const json = JSON.stringify(payload);
|
|
147
|
+
gzipCompress(json).then((compressed) => {
|
|
148
|
+
if (this.queue.length > 0) {
|
|
149
|
+
this.pendingBeaconBlob = new Blob([compressed], { type: "application/octet-stream" });
|
|
150
|
+
this.pendingBeaconUrl = `${this.config.serverUrl}/api/ingest?encoding=gzip`;
|
|
151
|
+
}
|
|
152
|
+
}).catch(() => {
|
|
153
|
+
});
|
|
154
|
+
}
|
|
70
155
|
buildPayload(events) {
|
|
71
156
|
return {
|
|
72
157
|
sessionId: this.config.cedarSessionId,
|
|
@@ -344,6 +429,7 @@ var consoleCapture = null;
|
|
|
344
429
|
var errorCapture = null;
|
|
345
430
|
var networkCapture = null;
|
|
346
431
|
var recorderCapture = null;
|
|
432
|
+
var unloadHandler = null;
|
|
347
433
|
function onEvent(event) {
|
|
348
434
|
transport?.enqueue(event);
|
|
349
435
|
}
|
|
@@ -372,8 +458,14 @@ var CedarReplay = {
|
|
|
372
458
|
networkCapture.start();
|
|
373
459
|
recorderCapture = new RecorderCapture(onEvent, config.recorder);
|
|
374
460
|
recorderCapture.start();
|
|
461
|
+
unloadHandler = () => transport?.flushOnUnload();
|
|
462
|
+
window.addEventListener("beforeunload", unloadHandler);
|
|
375
463
|
},
|
|
376
464
|
stop() {
|
|
465
|
+
if (unloadHandler) {
|
|
466
|
+
window.removeEventListener("beforeunload", unloadHandler);
|
|
467
|
+
unloadHandler = null;
|
|
468
|
+
}
|
|
377
469
|
recorderCapture?.stop();
|
|
378
470
|
recorderCapture = null;
|
|
379
471
|
networkCapture?.stop();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cedarai/session-replay-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
|
-
"prepare": "tsup src/index.ts --format esm --dts",
|
|
16
15
|
"build": "tsup src/index.ts --format esm --dts",
|
|
17
16
|
"build:iife": "tsup src/global.ts --format iife --outDir dist",
|
|
18
17
|
"dev": "tsup src/index.ts --format esm --dts --watch",
|