@cedarai/session-replay-sdk 0.1.0 → 0.3.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 +194 -12
- package/package.json +3 -3
- 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,
|
|
@@ -337,6 +422,91 @@ var RecorderCapture = class {
|
|
|
337
422
|
}
|
|
338
423
|
};
|
|
339
424
|
|
|
425
|
+
// src/performance.ts
|
|
426
|
+
import { onLCP, onCLS, onINP } from "web-vitals";
|
|
427
|
+
var MEMORY_INTERVAL_MS = 3e4;
|
|
428
|
+
var PerformanceCapture = class {
|
|
429
|
+
onEvent;
|
|
430
|
+
started = false;
|
|
431
|
+
longTaskObserver = null;
|
|
432
|
+
memoryTimer = null;
|
|
433
|
+
constructor(onEvent2) {
|
|
434
|
+
this.onEvent = onEvent2;
|
|
435
|
+
}
|
|
436
|
+
start() {
|
|
437
|
+
if (this.started) return;
|
|
438
|
+
this.started = true;
|
|
439
|
+
this.startWebVitals();
|
|
440
|
+
this.startLongTaskObserver();
|
|
441
|
+
this.startMemorySnapshots();
|
|
442
|
+
}
|
|
443
|
+
stop() {
|
|
444
|
+
if (!this.started) return;
|
|
445
|
+
this.started = false;
|
|
446
|
+
this.longTaskObserver?.disconnect();
|
|
447
|
+
this.longTaskObserver = null;
|
|
448
|
+
if (this.memoryTimer) {
|
|
449
|
+
clearInterval(this.memoryTimer);
|
|
450
|
+
this.memoryTimer = null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
emit(data) {
|
|
454
|
+
if (!this.started) return;
|
|
455
|
+
this.onEvent({
|
|
456
|
+
type: "performance",
|
|
457
|
+
timestamp: Date.now(),
|
|
458
|
+
data
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
startWebVitals() {
|
|
462
|
+
const handler = (metric) => {
|
|
463
|
+
this.emit({
|
|
464
|
+
metric: "web-vital",
|
|
465
|
+
name: metric.name,
|
|
466
|
+
value: metric.value,
|
|
467
|
+
rating: metric.rating
|
|
468
|
+
});
|
|
469
|
+
};
|
|
470
|
+
onLCP(handler);
|
|
471
|
+
onCLS(handler, { reportAllChanges: true });
|
|
472
|
+
onINP(handler, { reportAllChanges: true });
|
|
473
|
+
}
|
|
474
|
+
startLongTaskObserver() {
|
|
475
|
+
if (typeof globalThis.PerformanceObserver === "undefined") return;
|
|
476
|
+
try {
|
|
477
|
+
this.longTaskObserver = new globalThis.PerformanceObserver((list) => {
|
|
478
|
+
for (const entry of list.getEntries()) {
|
|
479
|
+
const attribution = entry.attribution?.[0]?.containerSrc;
|
|
480
|
+
this.emit({
|
|
481
|
+
metric: "long-task",
|
|
482
|
+
duration: entry.duration,
|
|
483
|
+
attribution
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
this.longTaskObserver.observe({ entryTypes: ["longtask"] });
|
|
488
|
+
} catch {
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
startMemorySnapshots() {
|
|
492
|
+
const memory = performance.memory;
|
|
493
|
+
if (!memory) return;
|
|
494
|
+
this.emitMemory(memory);
|
|
495
|
+
this.memoryTimer = setInterval(() => {
|
|
496
|
+
const mem = performance.memory;
|
|
497
|
+
if (mem) this.emitMemory(mem);
|
|
498
|
+
}, MEMORY_INTERVAL_MS);
|
|
499
|
+
}
|
|
500
|
+
emitMemory(memory) {
|
|
501
|
+
this.emit({
|
|
502
|
+
metric: "memory",
|
|
503
|
+
usedJSHeapSize: memory.usedJSHeapSize,
|
|
504
|
+
totalJSHeapSize: memory.totalJSHeapSize,
|
|
505
|
+
jsHeapSizeLimit: memory.jsHeapSizeLimit
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
|
|
340
510
|
// src/index.ts
|
|
341
511
|
var config = null;
|
|
342
512
|
var transport = null;
|
|
@@ -344,6 +514,8 @@ var consoleCapture = null;
|
|
|
344
514
|
var errorCapture = null;
|
|
345
515
|
var networkCapture = null;
|
|
346
516
|
var recorderCapture = null;
|
|
517
|
+
var performanceCapture = null;
|
|
518
|
+
var unloadHandler = null;
|
|
347
519
|
function onEvent(event) {
|
|
348
520
|
transport?.enqueue(event);
|
|
349
521
|
}
|
|
@@ -372,8 +544,18 @@ var CedarReplay = {
|
|
|
372
544
|
networkCapture.start();
|
|
373
545
|
recorderCapture = new RecorderCapture(onEvent, config.recorder);
|
|
374
546
|
recorderCapture.start();
|
|
547
|
+
performanceCapture = new PerformanceCapture(onEvent);
|
|
548
|
+
performanceCapture.start();
|
|
549
|
+
unloadHandler = () => transport?.flushOnUnload();
|
|
550
|
+
window.addEventListener("beforeunload", unloadHandler);
|
|
375
551
|
},
|
|
376
552
|
stop() {
|
|
553
|
+
if (unloadHandler) {
|
|
554
|
+
window.removeEventListener("beforeunload", unloadHandler);
|
|
555
|
+
unloadHandler = null;
|
|
556
|
+
}
|
|
557
|
+
performanceCapture?.stop();
|
|
558
|
+
performanceCapture = null;
|
|
377
559
|
recorderCapture?.stop();
|
|
378
560
|
recorderCapture = null;
|
|
379
561
|
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.3.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",
|
|
@@ -20,7 +19,8 @@
|
|
|
20
19
|
"test:watch": "vitest"
|
|
21
20
|
},
|
|
22
21
|
"dependencies": {
|
|
23
|
-
"rrweb": "^2.0.0-alpha.4"
|
|
22
|
+
"rrweb": "^2.0.0-alpha.4",
|
|
23
|
+
"web-vitals": "^4.2.4"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@vitest/browser": "^4.0.18",
|