@newtalaria/browser 0.1.6 → 0.1.7

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.
@@ -0,0 +1,18 @@
1
+ /** Durable tags on error events when an error-clip replay was attempted. */
2
+ export declare const REPLAY_CAPTURE_TAG = "replay.capture";
3
+ export declare const REPLAY_CAPTURE_REASON_TAG = "replay.capture_reason";
4
+ export type ReplayCaptureStatus = 'ok' | 'failed' | 'skipped';
5
+ export type ReplayCaptureReason = 'oversized_full_snapshot' | 'no_full_snapshot' | 'upload_failed' | 'not_sampled' | 'buffer_empty';
6
+ export interface ReplayCaptureFailure {
7
+ reason: ReplayCaptureReason;
8
+ details?: Record<string, unknown>;
9
+ }
10
+ export interface ReplayCaptureOutcome {
11
+ status: ReplayCaptureStatus;
12
+ reason?: ReplayCaptureReason;
13
+ details?: Record<string, unknown>;
14
+ }
15
+ /** Merge replay capture tags into event tags (error-clip path only). */
16
+ export declare function applyReplayCaptureTags(tags: Record<string, string>, outcome: ReplayCaptureOutcome | null): Record<string, string>;
17
+ export declare function mergeReplayCaptureExtra(extra: Record<string, unknown> | undefined, outcome: ReplayCaptureOutcome | null): Record<string, unknown> | undefined;
18
+ //# sourceMappingURL=capture_outcome.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture_outcome.d.ts","sourceRoot":"","sources":["../../src/replay/capture_outcome.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,mBAAmB,CAAC;AACnD,eAAO,MAAM,yBAAyB,0BAA0B,CAAC;AAEjE,MAAM,MAAM,mBAAmB,GAAG,IAAI,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE9D,MAAM,MAAM,mBAAmB,GAC3B,yBAAyB,GACzB,kBAAkB,GAClB,eAAe,GACf,aAAa,GACb,cAAc,CAAC;AAEnB,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,mBAAmB,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,mBAAmB,CAAC;IAC5B,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,wEAAwE;AACxE,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,oBAAoB,GAAG,IAAI,GACnC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQxB;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC1C,OAAO,EAAE,oBAAoB,GAAG,IAAI,GACnC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAarC"}
@@ -8,13 +8,13 @@ export interface RrwebEvent {
8
8
  * Soft uncompressed take size before gzip fitting.
9
9
  * Keep in sync with ReplayLimits.maxUncompressedSegmentBytes intent.
10
10
  */
11
- export declare const SEGMENT_SIZE_BYTES = 400000;
11
+ export declare const SEGMENT_SIZE_BYTES = 800000;
12
12
  /**
13
13
  * Server hard cap — keep in sync with ReplayLimits.maxCompressedSegmentBytes.
14
14
  * Restart Serverpod after changing this constant pair.
15
15
  */
16
16
  export declare const MAX_COMPRESSED_SEGMENT_BYTES: number;
17
- /** Pack under hard cap minus margin (never disagree with a live 512KiB server). */
17
+ /** Pack under hard cap minus margin (never disagree with a live 1MiB server). */
18
18
  export declare const TARGET_COMPRESSED_SEGMENT_BYTES: number;
19
19
  /**
20
20
  * Absolute max segments per replay (session / long error mode).
@@ -1 +1 @@
1
- {"version":3,"file":"segment_buffer.d.ts","sourceRoot":"","sources":["../../src/replay/segment_buffer.ts"],"names":[],"mappings":"AAEA,oDAAoD;AACpD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,SAAU,CAAC;AAC1C;;;GAGG;AACH,eAAO,MAAM,4BAA4B,QAAa,CAAC;AACvD,mFAAmF;AACnF,eAAO,MAAM,+BAA+B,QACF,CAAC;AAC3C;;;GAGG;AACH,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAC3C;;GAEG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAC1C,uEAAuE;AACvE,eAAO,MAAM,+BAA+B,QAAkB,CAAC;AAC/D,8DAA8D;AAC9D,eAAO,MAAM,yBAAyB,OAAQ,CAAC;AAC/C,+BAA+B;AAC/B,eAAO,MAAM,gBAAgB,OAAQ,CAAC;AACtC,oEAAoE;AACpE,eAAO,MAAM,cAAc,QAAS,CAAC;AACrC,6DAA6D;AAC7D,eAAO,MAAM,qBAAqB,UAAY,CAAC;AAC/C,2DAA2D;AAC3D,eAAO,MAAM,qBAAqB,QAAS,CAAC;AAC5C;;;GAGG;AACH,eAAO,MAAM,sBAAsB,QAAgB,CAAC;AAEpD,mCAAmC;AACnC,eAAO,MAAM,mBAAmB,IAAI,CAAC;AACrC,2BAA2B;AAC3B,eAAO,MAAM,UAAU,IAAI,CAAC;AAE5B,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,YAAY,CAAK;IAEzB,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAK7B,eAAe,IAAI,OAAO;IAI1B;;;OAGG;IACH,kBAAkB,IAAI,OAAO;IAc7B;;;OAGG;IACH,QAAQ,CAAC,GAAG,SAAa,EAAE,QAAQ,SAAwB,GAAG,IAAI;IAiBlE,iBAAiB,IAAI,OAAO;IAI5B;;;;OAIG;IACH,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,EAAE;IAyBpD,OAAO,IAAI,UAAU,EAAE;IAOvB,6EAA6E;IAC7E,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,IAAI;IAMnC,KAAK,IAAI,IAAI;IAKb,SAAS,IAAI;QAAE,SAAS,EAAE,IAAI,CAAC;QAAC,OAAO,EAAE,IAAI,CAAA;KAAE,GAAG,IAAI;IAUtD,OAAO,CAAC,cAAc;CAGvB"}
1
+ {"version":3,"file":"segment_buffer.d.ts","sourceRoot":"","sources":["../../src/replay/segment_buffer.ts"],"names":[],"mappings":"AAEA,oDAAoD;AACpD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,SAAU,CAAC;AAC1C;;;GAGG;AACH,eAAO,MAAM,4BAA4B,QAAc,CAAC;AACxD,iFAAiF;AACjF,eAAO,MAAM,+BAA+B,QACF,CAAC;AAC3C;;;GAGG;AACH,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAC3C;;GAEG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAC1C,uEAAuE;AACvE,eAAO,MAAM,+BAA+B,QAAkB,CAAC;AAC/D,8DAA8D;AAC9D,eAAO,MAAM,yBAAyB,OAAQ,CAAC;AAC/C,+BAA+B;AAC/B,eAAO,MAAM,gBAAgB,OAAQ,CAAC;AACtC,oEAAoE;AACpE,eAAO,MAAM,cAAc,QAAS,CAAC;AACrC,6DAA6D;AAC7D,eAAO,MAAM,qBAAqB,UAAY,CAAC;AAC/C,2DAA2D;AAC3D,eAAO,MAAM,qBAAqB,QAAS,CAAC;AAC5C;;;GAGG;AACH,eAAO,MAAM,sBAAsB,QAAgB,CAAC;AAEpD,mCAAmC;AACnC,eAAO,MAAM,mBAAmB,IAAI,CAAC;AACrC,2BAA2B;AAC3B,eAAO,MAAM,UAAU,IAAI,CAAC;AAE5B,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,YAAY,CAAK;IAEzB,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAK7B,eAAe,IAAI,OAAO;IAI1B;;;OAGG;IACH,kBAAkB,IAAI,OAAO;IAc7B;;;OAGG;IACH,QAAQ,CAAC,GAAG,SAAa,EAAE,QAAQ,SAAwB,GAAG,IAAI;IAiBlE,iBAAiB,IAAI,OAAO;IAI5B;;;;OAIG;IACH,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,EAAE;IAyBpD,OAAO,IAAI,UAAU,EAAE;IAOvB,6EAA6E;IAC7E,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,IAAI;IAMnC,KAAK,IAAI,IAAI;IAKb,SAAS,IAAI;QAAE,SAAS,EAAE,IAAI,CAAC;QAAC,OAAO,EAAE,IAAI,CAAA;KAAE,GAAG,IAAI;IAUtD,OAAO,CAAC,cAAc;CAGvB"}
@@ -123,9 +123,9 @@
123
123
  }
124
124
 
125
125
  // src/replay/segment_buffer.ts
126
- var SEGMENT_SIZE_BYTES = 4e5;
127
- var MAX_COMPRESSED_SEGMENT_BYTES = 512 * 1024;
128
- var TARGET_COMPRESSED_SEGMENT_BYTES = MAX_COMPRESSED_SEGMENT_BYTES - 32 * 1024;
126
+ var SEGMENT_SIZE_BYTES = 8e5;
127
+ var MAX_COMPRESSED_SEGMENT_BYTES = 1024 * 1024;
128
+ var TARGET_COMPRESSED_SEGMENT_BYTES = MAX_COMPRESSED_SEGMENT_BYTES - 64 * 1024;
129
129
  var MAX_SEGMENTS_PER_REPLAY = 200;
130
130
  var MAX_SEGMENTS_ERROR_CLIP = 12;
131
131
  var MAX_ERROR_CLIP_COMPRESSED_BYTES = 2 * 1024 * 1024;
@@ -357,6 +357,33 @@
357
357
  return null;
358
358
  }
359
359
 
360
+ // src/replay/capture_outcome.ts
361
+ var REPLAY_CAPTURE_TAG = "replay.capture";
362
+ var REPLAY_CAPTURE_REASON_TAG = "replay.capture_reason";
363
+ function applyReplayCaptureTags(tags, outcome) {
364
+ if (!outcome) return tags;
365
+ const next = { ...tags };
366
+ next[REPLAY_CAPTURE_TAG] = outcome.status;
367
+ if (outcome.reason) {
368
+ next[REPLAY_CAPTURE_REASON_TAG] = outcome.reason;
369
+ }
370
+ return next;
371
+ }
372
+ function mergeReplayCaptureExtra(extra, outcome) {
373
+ if (!outcome || outcome.status === "ok") {
374
+ return extra;
375
+ }
376
+ return {
377
+ ...extra ?? {},
378
+ replayCapture: {
379
+ attempted: outcome.status === "failed",
380
+ status: outcome.status,
381
+ reason: outcome.reason,
382
+ ...outcome.details ?? {}
383
+ }
384
+ };
385
+ }
386
+
360
387
  // ../../node_modules/rrweb/dist/rrweb.js
361
388
  var __defProp = Object.defineProperty;
362
389
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -13401,6 +13428,8 @@
13401
13428
  * attach it to the error event after budget/timer finish resets buffer mode.
13402
13429
  */
13403
13430
  this.linkableReplayId = null;
13431
+ /** Last error-clip paint/upload failure (cleared on successful segment 0). */
13432
+ this.lastReplayCaptureFailure = null;
13404
13433
  }
13405
13434
  init(options) {
13406
13435
  if (this.options) {
@@ -13422,6 +13451,7 @@
13422
13451
  this.uploadStartedAtMs = null;
13423
13452
  this.errorClipDeadlineMs = null;
13424
13453
  this.linkableReplayId = null;
13454
+ this.lastReplayCaptureFailure = null;
13425
13455
  this.buffer = new SegmentBuffer();
13426
13456
  this.uploadChain = Promise.resolve();
13427
13457
  this.sessionSampled = Math.random() < this.options.replaysSessionSampleRate;
@@ -13583,6 +13613,7 @@
13583
13613
  this.uploadChain = Promise.resolve();
13584
13614
  this.closed = false;
13585
13615
  this.linkableReplayId = null;
13616
+ this.lastReplayCaptureFailure = null;
13586
13617
  }
13587
13618
  onRrwebEvent(event) {
13588
13619
  if (this.closed || !this.options) return;
@@ -13608,15 +13639,22 @@
13608
13639
  throw new Error("@newtalaria/browser: call Talaria.init() first");
13609
13640
  }
13610
13641
  const isErrorLike = args.level === "error" || args.level === "fatal";
13642
+ let errorClipOutcome = null;
13643
+ let attemptedErrorClip = false;
13611
13644
  if (isErrorLike && !this.sessionSampled) {
13612
13645
  if (!this.uploadEnabled) {
13613
13646
  if (Math.random() < this.options.replaysOnErrorSampleRate) {
13647
+ attemptedErrorClip = true;
13648
+ this.lastReplayCaptureFailure = null;
13614
13649
  this.uploadEnabled = true;
13615
13650
  this.checkoutFullSnapshot();
13616
13651
  this.markUploadStarted();
13617
13652
  this.scheduleErrorClipEnd();
13653
+ } else {
13654
+ errorClipOutcome = { status: "skipped", reason: "not_sampled" };
13618
13655
  }
13619
13656
  } else {
13657
+ attemptedErrorClip = true;
13620
13658
  this.scheduleErrorClipEnd();
13621
13659
  }
13622
13660
  }
@@ -13625,15 +13663,49 @@
13625
13663
  await this.flush({ reason: "capture", keepalive: args.keepalive });
13626
13664
  } catch (error) {
13627
13665
  console.warn("@newtalaria/browser: replay flush failed", error);
13628
- throw error;
13666
+ this.lastReplayCaptureFailure = {
13667
+ reason: "upload_failed",
13668
+ details: {
13669
+ message: error instanceof Error ? error.message : String(error)
13670
+ }
13671
+ };
13672
+ if (!this.sessionSampled && this.segmentIndex === 0) {
13673
+ await this.abortUnusableClip("upload_failed", {
13674
+ message: error instanceof Error ? error.message : String(error)
13675
+ });
13676
+ }
13629
13677
  }
13630
13678
  }
13631
- const tags = {
13632
- ...this.options.tags ?? {},
13633
- ...args.context?.tags ?? {}
13634
- };
13635
- const extra = args.context?.extra;
13636
- const replayId = this.uploadEnabled && this.segmentIndex > 0 ? this.replayId : this.linkableReplayId;
13679
+ let replayId = this.uploadEnabled && this.segmentIndex > 0 ? this.replayId : this.linkableReplayId;
13680
+ if (this.lastReplayCaptureFailure && this.segmentIndex === 0) {
13681
+ replayId = null;
13682
+ }
13683
+ if (isErrorLike && !this.sessionSampled) {
13684
+ if (replayId) {
13685
+ errorClipOutcome = { status: "ok" };
13686
+ this.lastReplayCaptureFailure = null;
13687
+ } else if (errorClipOutcome?.status === "skipped") {
13688
+ } else if (this.lastReplayCaptureFailure) {
13689
+ errorClipOutcome = {
13690
+ status: "failed",
13691
+ reason: this.lastReplayCaptureFailure.reason,
13692
+ details: this.lastReplayCaptureFailure.details
13693
+ };
13694
+ } else if (attemptedErrorClip) {
13695
+ errorClipOutcome = { status: "failed", reason: "buffer_empty" };
13696
+ }
13697
+ }
13698
+ const tags = applyReplayCaptureTags(
13699
+ {
13700
+ ...this.options.tags ?? {},
13701
+ ...args.context?.tags ?? {}
13702
+ },
13703
+ errorClipOutcome
13704
+ );
13705
+ const extra = mergeReplayCaptureExtra(
13706
+ args.context?.extra,
13707
+ errorClipOutcome
13708
+ );
13637
13709
  try {
13638
13710
  await ingestEvent(this.transport, {
13639
13711
  message: args.message,
@@ -13645,7 +13717,7 @@
13645
13717
  release: this.options.release,
13646
13718
  userId: args.context?.userId ?? this.options.userId,
13647
13719
  sessionId: this.sessionId ?? void 0,
13648
- replayId,
13720
+ replayId: replayId ?? void 0,
13649
13721
  url: typeof location !== "undefined" ? location.href : void 0,
13650
13722
  tags: Object.keys(tags).length ? tags : void 0,
13651
13723
  extraJson: extra ? JSON.stringify(extra) : void 0,
@@ -13790,9 +13862,10 @@
13790
13862
  /**
13791
13863
  * Ensure segment 0 begins at Meta+FullSnapshot. Orphan increments alone
13792
13864
  * produce a blank rrweb player.
13865
+ * @returns false when no FullSnapshot is available.
13793
13866
  */
13794
13867
  prepareBufferForNewReplay() {
13795
- if (this.segmentIndex !== 0) return;
13868
+ if (this.segmentIndex !== 0) return true;
13796
13869
  if (!this.buffer.hasFullSnapshot()) {
13797
13870
  this.checkoutFullSnapshot();
13798
13871
  }
@@ -13800,6 +13873,40 @@
13800
13873
  console.warn(
13801
13874
  "@newtalaria/browser: replay has no FullSnapshot; player may show a blank screen"
13802
13875
  );
13876
+ return false;
13877
+ }
13878
+ return true;
13879
+ }
13880
+ /**
13881
+ * Stop an error clip that cannot paint (missing/oversized FullSnapshot).
13882
+ * Does not set linkableReplayId — the error event should not open a blank player.
13883
+ */
13884
+ async abortUnusableClip(reason, details) {
13885
+ this.lastReplayCaptureFailure = { reason, details };
13886
+ console.warn("@newtalaria/browser: aborting unusable replay clip", {
13887
+ reason,
13888
+ ...details
13889
+ });
13890
+ this.buffer.clear();
13891
+ this.linkableReplayId = null;
13892
+ if (this.startedOnServer && !this.finishedOnServer && this.transport && this.replayId) {
13893
+ try {
13894
+ await finishReplay(this.transport, {
13895
+ replayId: this.replayId,
13896
+ reason: `capture_failed:${reason}`,
13897
+ keepalive: false
13898
+ });
13899
+ this.finishedOnServer = true;
13900
+ } catch (error) {
13901
+ console.warn("@newtalaria/browser: replays/finish failed after abort", error);
13902
+ }
13903
+ }
13904
+ if (!this.sessionSampled) {
13905
+ this.resetToBufferMode();
13906
+ } else {
13907
+ this.uploadEnabled = false;
13908
+ this.clearErrorClipTimer();
13909
+ this.clearMaxDurationTimer();
13803
13910
  }
13804
13911
  }
13805
13912
  stopUploadingAfterLimit() {
@@ -13860,7 +13967,10 @@
13860
13967
  async uploadPendingSegments(opts) {
13861
13968
  if (!this.options || !this.transport || !this.replayId) return;
13862
13969
  if (!this.startedOnServer || this.finishedOnServer) return;
13863
- this.prepareBufferForNewReplay();
13970
+ if (!this.prepareBufferForNewReplay()) {
13971
+ await this.abortUnusableClip("no_full_snapshot");
13972
+ return;
13973
+ }
13864
13974
  while (this.buffer.length > 0) {
13865
13975
  if (this.isPastMaxDuration()) {
13866
13976
  break;
@@ -13900,7 +14010,17 @@
13900
14010
  }
13901
14011
  const fitted = await this.takeFittedSegment();
13902
14012
  if (!fitted) break;
14013
+ if (fitted.abortReason) {
14014
+ await this.abortUnusableClip(fitted.abortReason, fitted.abortDetails);
14015
+ return;
14016
+ }
13903
14017
  const { events, gzip } = fitted;
14018
+ if (this.segmentIndex === 0 && !events.some((e) => e.type === RRWEB_FULL_SNAPSHOT)) {
14019
+ await this.abortUnusableClip("no_full_snapshot", {
14020
+ segmentEventTypes: events.map((e) => e.type)
14021
+ });
14022
+ return;
14023
+ }
13904
14024
  if (this.isErrorClipMode() && this.uploadedCompressedBytes + gzip.length > MAX_ERROR_CLIP_COMPRESSED_BYTES && this.segmentIndex > 0) {
13905
14025
  this.buffer.clear();
13906
14026
  await this.finishOnServer({
@@ -13925,6 +14045,9 @@
13925
14045
  });
13926
14046
  this.segmentIndex = index2 + 1;
13927
14047
  this.uploadedCompressedBytes += gzip.length;
14048
+ if (index2 === 0) {
14049
+ this.lastReplayCaptureFailure = null;
14050
+ }
13928
14051
  } catch (error) {
13929
14052
  if (isTerminalReplayLimitError(error)) {
13930
14053
  console.warn(
@@ -13941,10 +14064,17 @@
13941
14064
  if (isOversizedSegmentError(error)) {
13942
14065
  const plan = planOversizedRetry(events);
13943
14066
  if (plan.action === "drop") {
14067
+ const droppedType = events[0]?.type;
13944
14068
  console.warn(
13945
14069
  "@newtalaria/browser: dropping rrweb event rejected as oversized",
13946
- { type: events[0]?.type }
14070
+ { type: droppedType }
13947
14071
  );
14072
+ if (this.segmentIndex === 0 && droppedType === RRWEB_FULL_SNAPSHOT) {
14073
+ await this.abortUnusableClip("oversized_full_snapshot", {
14074
+ source: "server_reject"
14075
+ });
14076
+ return;
14077
+ }
13948
14078
  continue;
13949
14079
  }
13950
14080
  this.buffer.prepend(plan.right);
@@ -13953,6 +14083,11 @@
13953
14083
  }
13954
14084
  this.buffer.prepend(events);
13955
14085
  console.warn("@newtalaria/browser: replays/ingestSegment failed", error);
14086
+ if (this.segmentIndex === 0) {
14087
+ await this.abortUnusableClip("upload_failed", {
14088
+ message: error instanceof Error ? error.message : String(error)
14089
+ });
14090
+ }
13956
14091
  break;
13957
14092
  }
13958
14093
  if (opts.keepalive) break;
@@ -13967,7 +14102,8 @@
13967
14102
  }
13968
14103
  /**
13969
14104
  * Pull the largest event prefix that gzips under the target compressed size.
13970
- * Drops a single event that can never fit.
14105
+ * Drops a single event that can never fit — except FullSnapshot on segment 0
14106
+ * (abort the clip instead of uploading a blank orphan stream).
13971
14107
  */
13972
14108
  async takeFittedSegment() {
13973
14109
  const chunk = this.buffer.takeByEstimatedBytes(SEGMENT_SIZE_BYTES);
@@ -13979,12 +14115,32 @@
13979
14115
  MAX_COMPRESSED_SEGMENT_BYTES
13980
14116
  );
13981
14117
  if (!fitted) {
14118
+ const dropped = chunk[0];
13982
14119
  console.warn(
13983
14120
  "@newtalaria/browser: dropping rrweb event that exceeds max segment size",
13984
- { type: chunk[0]?.type }
14121
+ { type: dropped?.type }
13985
14122
  );
14123
+ if (this.segmentIndex === 0 && dropped?.type === RRWEB_FULL_SNAPSHOT) {
14124
+ return {
14125
+ events: [],
14126
+ gzip: new Uint8Array(),
14127
+ abortReason: "oversized_full_snapshot",
14128
+ abortDetails: { source: "fit_compressed_prefix" }
14129
+ };
14130
+ }
14131
+ if (chunk.length > 1) {
14132
+ this.buffer.prepend(chunk.slice(1));
14133
+ }
13986
14134
  return this.takeFittedSegment();
13987
14135
  }
14136
+ if (this.segmentIndex === 0 && chunk.some((e) => e.type === RRWEB_FULL_SNAPSHOT) && !fitted.events.some((e) => e.type === RRWEB_FULL_SNAPSHOT)) {
14137
+ return {
14138
+ events: [],
14139
+ gzip: new Uint8Array(),
14140
+ abortReason: "oversized_full_snapshot",
14141
+ abortDetails: { source: "fit_dropped_full_snapshot" }
14142
+ };
14143
+ }
13988
14144
  if (fitted.remainder.length > 0) {
13989
14145
  this.buffer.prepend(fitted.remainder);
13990
14146
  }