@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 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
- this.fetchFn(`${this.config.serverUrl}/api/ingest`, {
51
- method: "POST",
52
- headers: { "Content-Type": "application/json" },
53
- body: JSON.stringify(payload)
54
- }).catch(() => {
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. sendBeacon cannot set custom headers,
59
- * so we send as a Blob and indicate gzip via query param.
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 events = this.queue.splice(0);
64
- const payload = this.buildPayload(events);
65
- const body = JSON.stringify(payload);
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.1.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",