@nagoorkani/session-capture-vue-replay 0.1.0 → 0.1.2

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
@@ -18,7 +18,7 @@ import { isSampled, ReplayGate, safe } from '@nagoorkani/session-capture-core';
18
18
  import { currentConfig, keepSessionAlive, onError, onSession, } from '@nagoorkani/session-capture-vue';
19
19
  import { record } from 'rrweb';
20
20
  import { Recorder } from "./recorder.js";
21
- import { uploadSegment } from "./upload.js";
21
+ import { splitSegment, uploadSegment } from "./upload.js";
22
22
  /** Set by `installReplay`; read by the flusher, which outlives one call. */
23
23
  const uploadOptions = {
24
24
  endpoint: '',
@@ -182,17 +182,25 @@ async function flushNow(unloading = false) {
182
182
  keepSessionAlive();
183
183
  sending = true;
184
184
  try {
185
- const result = await uploadSegment({
186
- endpoint: uploadOptions.endpoint,
187
- apiKey: uploadOptions.apiKey,
188
- sessionId,
189
- keepalive: unloading,
190
- }, segment);
191
- // One more attempt on the next tick, then it is gone. Holding a failing
192
- // segment forever is how an offline tab runs out of memory.
193
- if (result.kind === 'failed' && !unloading)
194
- retrying = segment;
195
- uploadOptions.onUpload?.(result, segment.length);
185
+ const chunks = splitSegment(segment);
186
+ for (let i = 0; i < chunks.length; i++) {
187
+ const chunk = chunks[i];
188
+ const result = await uploadSegment({
189
+ endpoint: uploadOptions.endpoint,
190
+ apiKey: uploadOptions.apiKey,
191
+ sessionId,
192
+ keepalive: unloading,
193
+ }, chunk);
194
+ uploadOptions.onUpload?.(result, chunk.length);
195
+ // Kept for the next tick, unloading or not: a tab switch fires the same
196
+ // event as a close, and a tab that comes back should still have its
197
+ // full snapshot. A tab that really closed takes the memory with it.
198
+ // Kept in order, because rrweb's stream is only meaningful in order.
199
+ if (result.kind === 'failed') {
200
+ retrying = chunks.slice(i).flat();
201
+ break;
202
+ }
203
+ }
196
204
  }
197
205
  finally {
198
206
  sending = false;
package/dist/upload.d.ts CHANGED
@@ -20,6 +20,25 @@ export interface UploadOptions {
20
20
  /** Ask the browser to finish this one after the page is gone. */
21
21
  keepalive?: boolean;
22
22
  }
23
+ /**
24
+ * Browsers refuse a `keepalive` request whose body would take the page's
25
+ * in-flight keepalive bytes over 64 KB — the fetch rejects at once. The first
26
+ * segment carries the full DOM snapshot and is almost always larger, so it
27
+ * went out as keepalive on a tab switch and was lost, leaving every later
28
+ * segment as mutations with nothing to apply them to. Above this, send it
29
+ * as an ordinary request: a hidden tab usually still runs, and that is the
30
+ * common case.
31
+ */
32
+ export declare const KEEPALIVE_MAX_BYTES: number;
33
+ /**
34
+ * Ingestion's per-upload limits: `MAX_FRAMES` in `event-schema`'s replay
35
+ * container, and half its 8 MB body limit. A busy page emits thousands of
36
+ * rrweb events in thirty seconds, and one oversized upload is rejected whole.
37
+ */
38
+ export declare const MAX_UPLOAD_EVENTS = 2000;
39
+ export declare const MAX_UPLOAD_BYTES: number;
40
+ /** Split a segment into uploads ingestion will accept, in order. */
41
+ export declare function splitSegment(segment: readonly Segment[]): Segment[][];
23
42
  export type UploadResult = {
24
43
  kind: 'sent';
25
44
  events: number;
package/dist/upload.js CHANGED
@@ -12,6 +12,42 @@
12
12
  * shows a recording worth the quota.
13
13
  */
14
14
  import { encodeContainer, MAGIC_DOM } from '@nagoorkani/session-capture-core';
15
+ /**
16
+ * Browsers refuse a `keepalive` request whose body would take the page's
17
+ * in-flight keepalive bytes over 64 KB — the fetch rejects at once. The first
18
+ * segment carries the full DOM snapshot and is almost always larger, so it
19
+ * went out as keepalive on a tab switch and was lost, leaving every later
20
+ * segment as mutations with nothing to apply them to. Above this, send it
21
+ * as an ordinary request: a hidden tab usually still runs, and that is the
22
+ * common case.
23
+ */
24
+ export const KEEPALIVE_MAX_BYTES = 60 * 1024;
25
+ /**
26
+ * Ingestion's per-upload limits: `MAX_FRAMES` in `event-schema`'s replay
27
+ * container, and half its 8 MB body limit. A busy page emits thousands of
28
+ * rrweb events in thirty seconds, and one oversized upload is rejected whole.
29
+ */
30
+ export const MAX_UPLOAD_EVENTS = 2000;
31
+ export const MAX_UPLOAD_BYTES = 4 * 1024 * 1024;
32
+ /** Split a segment into uploads ingestion will accept, in order. */
33
+ export function splitSegment(segment) {
34
+ const chunks = [];
35
+ let chunk = [];
36
+ let bytes = 0;
37
+ for (const event of segment) {
38
+ if (chunk.length > 0 &&
39
+ (chunk.length >= MAX_UPLOAD_EVENTS || bytes + event.payload.byteLength > MAX_UPLOAD_BYTES)) {
40
+ chunks.push(chunk);
41
+ chunk = [];
42
+ bytes = 0;
43
+ }
44
+ chunk.push(event);
45
+ bytes += event.payload.byteLength;
46
+ }
47
+ if (chunk.length > 0)
48
+ chunks.push(chunk);
49
+ return chunks;
50
+ }
15
51
  export async function uploadSegment(options, segment) {
16
52
  if (segment.length === 0)
17
53
  return { kind: 'skipped', reason: 'no events' };
@@ -35,7 +71,7 @@ export async function uploadSegment(options, segment) {
35
71
  authorization: `Bearer ${options.apiKey}`,
36
72
  },
37
73
  body: body,
38
- keepalive: options.keepalive === true,
74
+ keepalive: options.keepalive === true && body.byteLength <= KEEPALIVE_MAX_BYTES,
39
75
  });
40
76
  return response.ok
41
77
  ? { kind: 'sent', events: segment.length }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nagoorkani/session-capture-vue-replay",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Optional DOM session replay for @nagoorkani/session-capture-vue",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -19,11 +19,11 @@
19
19
  "LICENSE"
20
20
  ],
21
21
  "dependencies": {
22
- "@nagoorkani/session-capture-core": "0.1.0"
22
+ "@nagoorkani/session-capture-core": "^0.1.4"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "rrweb": "^2.0.0",
26
- "@nagoorkani/session-capture-vue": "0.1.0"
26
+ "@nagoorkani/session-capture-vue": "^0.1.2"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^22.0.0",