@newtalaria/browser 0.1.8 → 0.1.10

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/README.md CHANGED
@@ -99,7 +99,7 @@ You pay for **uploaded + retained** bytes, not for local buffering. Prefer error
99
99
  | Error sample + `replaysErrorAfterMs > 0` | Upload buffer (≤~960KiB gzip pack target / **1MiB** server cap) + trailing window, hard caps **12 segments** or **2MiB** compressed, attach `replayId` only if segments landed, `finish`, return to buffer mode |
100
100
  | Error sample + `replaysErrorAfterMs = 0` | Upload buffer then continue like session mode until **5 min** / unload / size caps |
101
101
  | Server limit (replay segments / total size / duration) | Stop uploading that replay; no retries |
102
- | Oversized FullSnapshot on segment 0 | **Abort** the clip (no blank orphan upload); error event gets `replay.capture=failed` + reason |
102
+ | Oversized FullSnapshot on segment 0 | **Abort** the clip (no blank orphan upload); error event gets `replay.capture=failed` + reason. Meta+FullSnapshot are taken/fitted **atomically** so a soft estimated take window cannot orphan the snapshot. |
103
103
  | Oversized single non-FS rrweb event | Dropped with a console warning (cannot fit under segment cap) |
104
104
  | `pagehide` / `close` | Flush pending segments with `fetch` `keepalive`, then `replays/finish`; `close` fully resets so `init()` works again (React Strict Mode) |
105
105
 
@@ -114,6 +114,17 @@ When an **error/fatal** event attempts an on-error clip (`replaysSessionSampleRa
114
114
 
115
115
  Failed captures do **not** set `replayId` (avoids linking a blank player). Details are also under `extra.replayCapture`.
116
116
 
117
+ When the paint base cannot ship, `extra.replayCapture` also includes size diagnostics where available:
118
+
119
+ | Field | Meaning |
120
+ | --- | --- |
121
+ | `fullSnapshotEstimatedBytes` | JSON size estimate of the FullSnapshot |
122
+ | `fullSnapshotCompressedBytes` | Gzip size of the FullSnapshot alone (when measured) |
123
+ | `metaEstimatedBytes` | JSON size estimate of the Meta event |
124
+ | `maxUncompressedSegmentBytes` / `maxCompressedSegmentBytes` | Current pack caps |
125
+
126
+ Use these to see how far over budget a CMS/admin snapshot was when a clip was aborted.
127
+
117
128
  ## Serverpod URL pattern
118
129
 
119
130
  Talaria uses **Serverpod RPC**, not REST resource URLs:
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { CaptureContext, SeverityLevel, TalariaInitOptions } from './types.js';
2
- export { computeErrorClipDeadlineMs, fitCompressedPrefix, isErrorClipBudgetExhausted, planOversizedRetry, } from './replay/fit_segment.js';
2
+ export { computeErrorClipDeadlineMs, fitCompressedPrefix, isErrorClipBudgetExhausted, paintBaseEventCount, planOversizedRetry, } from './replay/fit_segment.js';
3
3
  export { MAX_SEGMENTS_ERROR_CLIP, MAX_ERROR_CLIP_COMPRESSED_BYTES, TARGET_COMPRESSED_SEGMENT_BYTES, MAX_COMPRESSED_SEGMENT_BYTES, } from './replay/segment_buffer.js';
4
4
  export { REPLAY_CAPTURE_TAG, REPLAY_CAPTURE_REASON_TAG, applyReplayCaptureTags, mergeReplayCaptureExtra, } from './replay/capture_outcome.js';
5
5
  export type { ReplayCaptureOutcome, ReplayCaptureReason, ReplayCaptureStatus, } from './replay/capture_outcome.js';
@@ -89,8 +89,9 @@ export declare class TalariaClient {
89
89
  private maxSegmentsAllowed;
90
90
  /**
91
91
  * Pull the largest event prefix that gzips under the target compressed size.
92
- * Drops a single event that can never fit — except FullSnapshot on segment 0
93
- * (abort the clip instead of uploading a blank orphan stream).
92
+ *
93
+ * Segment 0 keeps Meta+FullSnapshot (paint base) atomic through take + fit so
94
+ * a soft estimated-byte window cannot orphan the FullSnapshot.
94
95
  */
95
96
  private takeFittedSegment;
96
97
  private finishOnServer;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -161,6 +161,10 @@ var SegmentBuffer = class {
161
161
  hasFullSnapshot() {
162
162
  return this.events.some((e) => e.type === RRWEB_FULL_SNAPSHOT);
163
163
  }
164
+ /** First FullSnapshot still queued (does not remove it). */
165
+ peekFullSnapshot() {
166
+ return this.events.find((e) => e.type === RRWEB_FULL_SNAPSHOT);
167
+ }
164
168
  /**
165
169
  * Drop leading events until Meta+FullSnapshot (or FullSnapshot alone).
166
170
  * Returns false if no FullSnapshot is present (replay would paint blank).
@@ -200,15 +204,44 @@ var SegmentBuffer = class {
200
204
  shouldFlushBySize() {
201
205
  return this.pendingBytes >= SEGMENT_SIZE_BYTES;
202
206
  }
207
+ /**
208
+ * How many leading events form the rrweb paint base (Meta+FullSnapshot or
209
+ * FullSnapshot alone). Returns 0 if the buffer does not start with a paint base.
210
+ */
211
+ paintBaseLength() {
212
+ if (this.events.length === 0) return 0;
213
+ if (this.events[0].type === RRWEB_META && this.events[1]?.type === RRWEB_FULL_SNAPSHOT) {
214
+ return 2;
215
+ }
216
+ if (this.events[0].type === RRWEB_FULL_SNAPSHOT) {
217
+ return 1;
218
+ }
219
+ return 0;
220
+ }
203
221
  /**
204
222
  * Take a prefix of events whose estimated JSON size is ~maxBytes.
205
223
  * Always returns at least one event when the buffer is non-empty (caller
206
224
  * must handle a single oversized event).
225
+ *
226
+ * When `keepPaintBaseTogether` is true (segment 0), Meta immediately followed
227
+ * by FullSnapshot is taken as one unit even if estimated size exceeds
228
+ * maxBytes — gzip fitting / hard caps decide whether it can ship.
207
229
  */
208
- takeByEstimatedBytes(maxBytes) {
230
+ takeByEstimatedBytes(maxBytes, opts) {
209
231
  if (this.events.length === 0) return [];
210
232
  const batch = [];
211
233
  let batchBytes = 0;
234
+ if (opts?.keepPaintBaseTogether) {
235
+ const paintLen = this.paintBaseLength();
236
+ if (paintLen > 0) {
237
+ for (let i = 0; i < paintLen; i++) {
238
+ const ev = this.events.shift();
239
+ batch.push(ev);
240
+ batchBytes += estimateJsonBytes(ev);
241
+ }
242
+ this.recomputeBytes();
243
+ }
244
+ }
212
245
  while (this.events.length > 0) {
213
246
  const next = this.events[0];
214
247
  const nextBytes = estimateJsonBytes(next);
@@ -333,8 +366,53 @@ function planOversizedRetry(events) {
333
366
  right: events.slice(mid)
334
367
  };
335
368
  }
336
- async function fitCompressedPrefix(events, compress, targetBytes = TARGET_COMPRESSED_SEGMENT_BYTES, hardCap = MAX_COMPRESSED_SEGMENT_BYTES) {
369
+ function paintBaseEventCount(events) {
370
+ if (events.length === 0) return 0;
371
+ if (events[0].type === RRWEB_META && events[1]?.type === RRWEB_FULL_SNAPSHOT) {
372
+ return 2;
373
+ }
374
+ if (events[0].type === RRWEB_FULL_SNAPSHOT) {
375
+ return 1;
376
+ }
377
+ return 0;
378
+ }
379
+ async function fitCompressedPrefix(events, compress, targetBytes = TARGET_COMPRESSED_SEGMENT_BYTES, hardCap = MAX_COMPRESSED_SEGMENT_BYTES, opts) {
337
380
  if (events.length === 0) return null;
381
+ const atomicMin = opts?.atomicMinCount ?? 0;
382
+ if (atomicMin > 0) {
383
+ if (events.length < atomicMin) return null;
384
+ const paint = events.slice(0, atomicMin);
385
+ const paintGzip = await compress(paint);
386
+ if (paintGzip.length > hardCap) {
387
+ return null;
388
+ }
389
+ let lo = atomicMin;
390
+ let hi = events.length;
391
+ let best = {
392
+ events: paint,
393
+ gzip: paintGzip
394
+ };
395
+ while (lo <= hi) {
396
+ const mid = Math.floor((lo + hi) / 2);
397
+ if (mid < atomicMin) {
398
+ lo = atomicMin;
399
+ continue;
400
+ }
401
+ const prefix = events.slice(0, mid);
402
+ const gzip = await compress(prefix);
403
+ if (gzip.length <= targetBytes && gzip.length <= hardCap) {
404
+ best = { events: prefix, gzip };
405
+ lo = mid + 1;
406
+ } else {
407
+ hi = mid - 1;
408
+ }
409
+ }
410
+ return {
411
+ events: best.events,
412
+ gzip: best.gzip,
413
+ remainder: events.slice(best.events.length)
414
+ };
415
+ }
338
416
  let remaining = events;
339
417
  while (remaining.length > 0) {
340
418
  let lo = 1;
@@ -390,6 +468,28 @@ function mergeReplayCaptureExtra(extra, outcome) {
390
468
  };
391
469
  }
392
470
 
471
+ // src/replay/paint_base_size.ts
472
+ function paintBaseSizeDetails(events, opts) {
473
+ const meta = events.find((e) => e.type === RRWEB_META);
474
+ const full = events.find((e) => e.type === RRWEB_FULL_SNAPSHOT);
475
+ const details = {
476
+ segmentEventTypes: events.map((e) => e.type),
477
+ softTakeEstimatedBytes: SEGMENT_SIZE_BYTES,
478
+ targetCompressedSegmentBytes: TARGET_COMPRESSED_SEGMENT_BYTES,
479
+ maxCompressedSegmentBytes: MAX_COMPRESSED_SEGMENT_BYTES
480
+ };
481
+ if (meta) {
482
+ details.metaEstimatedBytes = estimateJsonBytes(meta);
483
+ }
484
+ if (full) {
485
+ details.fullSnapshotEstimatedBytes = estimateJsonBytes(full);
486
+ }
487
+ if (opts?.fullSnapshotCompressedBytes !== void 0) {
488
+ details.fullSnapshotCompressedBytes = opts.fullSnapshotCompressedBytes;
489
+ }
490
+ return details;
491
+ }
492
+
393
493
  // ../../node_modules/rrweb/dist/rrweb.js
394
494
  var __defProp = Object.defineProperty;
395
495
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -13975,7 +14075,11 @@ var TalariaClient = class {
13975
14075
  if (!this.options || !this.transport || !this.replayId) return;
13976
14076
  if (!this.startedOnServer || this.finishedOnServer) return;
13977
14077
  if (!this.prepareBufferForNewReplay()) {
13978
- await this.abortUnusableClip("no_full_snapshot");
14078
+ const leftover = this.buffer.peekFullSnapshot();
14079
+ await this.abortUnusableClip(
14080
+ "no_full_snapshot",
14081
+ paintBaseSizeDetails(leftover ? [leftover] : [])
14082
+ );
13979
14083
  return;
13980
14084
  }
13981
14085
  while (this.buffer.length > 0) {
@@ -14023,9 +14127,10 @@ var TalariaClient = class {
14023
14127
  }
14024
14128
  const { events, gzip } = fitted;
14025
14129
  if (this.segmentIndex === 0 && !events.some((e) => e.type === RRWEB_FULL_SNAPSHOT)) {
14026
- await this.abortUnusableClip("no_full_snapshot", {
14027
- segmentEventTypes: events.map((e) => e.type)
14028
- });
14130
+ await this.abortUnusableClip(
14131
+ "no_full_snapshot",
14132
+ paintBaseSizeDetails(events)
14133
+ );
14029
14134
  return;
14030
14135
  }
14031
14136
  if (this.isErrorClipMode() && this.uploadedCompressedBytes + gzip.length > MAX_ERROR_CLIP_COMPRESSED_BYTES && this.segmentIndex > 0) {
@@ -14069,6 +14174,14 @@ var TalariaClient = class {
14069
14174
  break;
14070
14175
  }
14071
14176
  if (isOversizedSegmentError(error)) {
14177
+ if (this.segmentIndex === 0 && paintBaseEventCount(events) > 0) {
14178
+ await this.abortUnusableClip("oversized_full_snapshot", {
14179
+ source: "server_reject_paint_base",
14180
+ ...paintBaseSizeDetails(events),
14181
+ rejectedCompressedBytes: gzip.length
14182
+ });
14183
+ return;
14184
+ }
14072
14185
  const plan = planOversizedRetry(events);
14073
14186
  if (plan.action === "drop") {
14074
14187
  const droppedType = events[0]?.type;
@@ -14076,10 +14189,15 @@ var TalariaClient = class {
14076
14189
  "@newtalaria/browser: dropping rrweb event rejected as oversized",
14077
14190
  { type: droppedType }
14078
14191
  );
14079
- if (this.segmentIndex === 0 && droppedType === RRWEB_FULL_SNAPSHOT) {
14080
- await this.abortUnusableClip("oversized_full_snapshot", {
14081
- source: "server_reject"
14082
- });
14192
+ if (droppedType === RRWEB_FULL_SNAPSHOT) {
14193
+ await this.abortUnusableClip(
14194
+ "oversized_full_snapshot",
14195
+ {
14196
+ source: "server_reject",
14197
+ ...paintBaseSizeDetails(events),
14198
+ rejectedCompressedBytes: gzip.length
14199
+ }
14200
+ );
14083
14201
  return;
14084
14202
  }
14085
14203
  continue;
@@ -14109,45 +14227,60 @@ var TalariaClient = class {
14109
14227
  }
14110
14228
  /**
14111
14229
  * Pull the largest event prefix that gzips under the target compressed size.
14112
- * Drops a single event that can never fit — except FullSnapshot on segment 0
14113
- * (abort the clip instead of uploading a blank orphan stream).
14230
+ *
14231
+ * Segment 0 keeps Meta+FullSnapshot (paint base) atomic through take + fit so
14232
+ * a soft estimated-byte window cannot orphan the FullSnapshot.
14114
14233
  */
14115
14234
  async takeFittedSegment() {
14116
- const chunk = this.buffer.takeByEstimatedBytes(SEGMENT_SIZE_BYTES);
14235
+ const isPaintBaseSegment = this.segmentIndex === 0;
14236
+ const chunk = this.buffer.takeByEstimatedBytes(SEGMENT_SIZE_BYTES, {
14237
+ keepPaintBaseTogether: isPaintBaseSegment
14238
+ });
14117
14239
  if (chunk.length === 0) return null;
14240
+ const atomicMin = isPaintBaseSegment ? paintBaseEventCount(chunk) : 0;
14118
14241
  const fitted = await fitCompressedPrefix(
14119
14242
  chunk,
14120
14243
  compressReplayEvents,
14121
14244
  TARGET_COMPRESSED_SEGMENT_BYTES,
14122
- MAX_COMPRESSED_SEGMENT_BYTES
14245
+ MAX_COMPRESSED_SEGMENT_BYTES,
14246
+ atomicMin > 0 ? { atomicMinCount: atomicMin } : void 0
14123
14247
  );
14124
14248
  if (!fitted) {
14125
- const dropped = chunk[0];
14126
- console.warn(
14127
- "@newtalaria/browser: dropping rrweb event that exceeds max segment size",
14128
- { type: dropped?.type }
14129
- );
14130
- if (this.segmentIndex === 0 && dropped?.type === RRWEB_FULL_SNAPSHOT) {
14249
+ if (isPaintBaseSegment && atomicMin > 0) {
14250
+ const paint = chunk.slice(0, atomicMin);
14251
+ let compressedBytes;
14252
+ const full = paint.find((e) => e.type === RRWEB_FULL_SNAPSHOT);
14253
+ if (full) {
14254
+ try {
14255
+ compressedBytes = (await compressReplayEvents([full])).length;
14256
+ } catch {
14257
+ }
14258
+ }
14259
+ if (chunk.length > atomicMin) {
14260
+ this.buffer.prepend(chunk.slice(atomicMin));
14261
+ }
14131
14262
  return {
14132
14263
  events: [],
14133
14264
  gzip: new Uint8Array(),
14134
14265
  abortReason: "oversized_full_snapshot",
14135
- abortDetails: { source: "fit_compressed_prefix" }
14266
+ abortDetails: {
14267
+ source: "paint_base_exceeds_hard_cap",
14268
+ ...paintBaseSizeDetails(paint, {
14269
+ fullSnapshotCompressedBytes: compressedBytes
14270
+ })
14271
+ }
14136
14272
  };
14137
14273
  }
14274
+ const dropped = chunk[0];
14275
+ console.warn(
14276
+ "@newtalaria/browser: dropping rrweb event that exceeds max segment size",
14277
+ { type: dropped?.type }
14278
+ );
14138
14279
  if (chunk.length > 1) {
14139
14280
  this.buffer.prepend(chunk.slice(1));
14140
14281
  }
14141
14282
  return this.takeFittedSegment();
14142
14283
  }
14143
- if (this.segmentIndex === 0 && chunk.some((e) => e.type === RRWEB_FULL_SNAPSHOT) && !fitted.events.some((e) => e.type === RRWEB_FULL_SNAPSHOT)) {
14144
- return {
14145
- events: [],
14146
- gzip: new Uint8Array(),
14147
- abortReason: "oversized_full_snapshot",
14148
- abortDetails: { source: "fit_dropped_full_snapshot" }
14149
- };
14150
- }
14151
14284
  if (fitted.remainder.length > 0) {
14152
14285
  this.buffer.prepend(fitted.remainder);
14153
14286
  }