@newtalaria/browser 0.1.6 → 0.1.8

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"}
@@ -17,6 +17,14 @@
17
17
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
18
18
  }
19
19
 
20
+ // src/utils/opaque_script_error.ts
21
+ function isOpaqueCrossOriginScriptError(event) {
22
+ const msg = (event.message ?? "").trim();
23
+ if (!/^script error\.?$/i.test(msg)) return false;
24
+ if (event.error instanceof Error) return false;
25
+ return true;
26
+ }
27
+
20
28
  // src/transport/serverpod.ts
21
29
  var ServerpodTransport = class {
22
30
  constructor(options) {
@@ -123,9 +131,9 @@
123
131
  }
124
132
 
125
133
  // 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;
134
+ var SEGMENT_SIZE_BYTES = 8e5;
135
+ var MAX_COMPRESSED_SEGMENT_BYTES = 1024 * 1024;
136
+ var TARGET_COMPRESSED_SEGMENT_BYTES = MAX_COMPRESSED_SEGMENT_BYTES - 64 * 1024;
129
137
  var MAX_SEGMENTS_PER_REPLAY = 200;
130
138
  var MAX_SEGMENTS_ERROR_CLIP = 12;
131
139
  var MAX_ERROR_CLIP_COMPRESSED_BYTES = 2 * 1024 * 1024;
@@ -357,6 +365,33 @@
357
365
  return null;
358
366
  }
359
367
 
368
+ // src/replay/capture_outcome.ts
369
+ var REPLAY_CAPTURE_TAG = "replay.capture";
370
+ var REPLAY_CAPTURE_REASON_TAG = "replay.capture_reason";
371
+ function applyReplayCaptureTags(tags, outcome) {
372
+ if (!outcome) return tags;
373
+ const next = { ...tags };
374
+ next[REPLAY_CAPTURE_TAG] = outcome.status;
375
+ if (outcome.reason) {
376
+ next[REPLAY_CAPTURE_REASON_TAG] = outcome.reason;
377
+ }
378
+ return next;
379
+ }
380
+ function mergeReplayCaptureExtra(extra, outcome) {
381
+ if (!outcome || outcome.status === "ok") {
382
+ return extra;
383
+ }
384
+ return {
385
+ ...extra ?? {},
386
+ replayCapture: {
387
+ attempted: outcome.status === "failed",
388
+ status: outcome.status,
389
+ reason: outcome.reason,
390
+ ...outcome.details ?? {}
391
+ }
392
+ };
393
+ }
394
+
360
395
  // ../../node_modules/rrweb/dist/rrweb.js
361
396
  var __defProp = Object.defineProperty;
362
397
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -13401,6 +13436,8 @@
13401
13436
  * attach it to the error event after budget/timer finish resets buffer mode.
13402
13437
  */
13403
13438
  this.linkableReplayId = null;
13439
+ /** Last error-clip paint/upload failure (cleared on successful segment 0). */
13440
+ this.lastReplayCaptureFailure = null;
13404
13441
  }
13405
13442
  init(options) {
13406
13443
  if (this.options) {
@@ -13422,6 +13459,7 @@
13422
13459
  this.uploadStartedAtMs = null;
13423
13460
  this.errorClipDeadlineMs = null;
13424
13461
  this.linkableReplayId = null;
13462
+ this.lastReplayCaptureFailure = null;
13425
13463
  this.buffer = new SegmentBuffer();
13426
13464
  this.uploadChain = Promise.resolve();
13427
13465
  this.sessionSampled = Math.random() < this.options.replaysSessionSampleRate;
@@ -13583,6 +13621,7 @@
13583
13621
  this.uploadChain = Promise.resolve();
13584
13622
  this.closed = false;
13585
13623
  this.linkableReplayId = null;
13624
+ this.lastReplayCaptureFailure = null;
13586
13625
  }
13587
13626
  onRrwebEvent(event) {
13588
13627
  if (this.closed || !this.options) return;
@@ -13608,15 +13647,22 @@
13608
13647
  throw new Error("@newtalaria/browser: call Talaria.init() first");
13609
13648
  }
13610
13649
  const isErrorLike = args.level === "error" || args.level === "fatal";
13650
+ let errorClipOutcome = null;
13651
+ let attemptedErrorClip = false;
13611
13652
  if (isErrorLike && !this.sessionSampled) {
13612
13653
  if (!this.uploadEnabled) {
13613
13654
  if (Math.random() < this.options.replaysOnErrorSampleRate) {
13655
+ attemptedErrorClip = true;
13656
+ this.lastReplayCaptureFailure = null;
13614
13657
  this.uploadEnabled = true;
13615
13658
  this.checkoutFullSnapshot();
13616
13659
  this.markUploadStarted();
13617
13660
  this.scheduleErrorClipEnd();
13661
+ } else {
13662
+ errorClipOutcome = { status: "skipped", reason: "not_sampled" };
13618
13663
  }
13619
13664
  } else {
13665
+ attemptedErrorClip = true;
13620
13666
  this.scheduleErrorClipEnd();
13621
13667
  }
13622
13668
  }
@@ -13625,15 +13671,49 @@
13625
13671
  await this.flush({ reason: "capture", keepalive: args.keepalive });
13626
13672
  } catch (error) {
13627
13673
  console.warn("@newtalaria/browser: replay flush failed", error);
13628
- throw error;
13674
+ this.lastReplayCaptureFailure = {
13675
+ reason: "upload_failed",
13676
+ details: {
13677
+ message: error instanceof Error ? error.message : String(error)
13678
+ }
13679
+ };
13680
+ if (!this.sessionSampled && this.segmentIndex === 0) {
13681
+ await this.abortUnusableClip("upload_failed", {
13682
+ message: error instanceof Error ? error.message : String(error)
13683
+ });
13684
+ }
13629
13685
  }
13630
13686
  }
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;
13687
+ let replayId = this.uploadEnabled && this.segmentIndex > 0 ? this.replayId : this.linkableReplayId;
13688
+ if (this.lastReplayCaptureFailure && this.segmentIndex === 0) {
13689
+ replayId = null;
13690
+ }
13691
+ if (isErrorLike && !this.sessionSampled) {
13692
+ if (replayId) {
13693
+ errorClipOutcome = { status: "ok" };
13694
+ this.lastReplayCaptureFailure = null;
13695
+ } else if (errorClipOutcome?.status === "skipped") {
13696
+ } else if (this.lastReplayCaptureFailure) {
13697
+ errorClipOutcome = {
13698
+ status: "failed",
13699
+ reason: this.lastReplayCaptureFailure.reason,
13700
+ details: this.lastReplayCaptureFailure.details
13701
+ };
13702
+ } else if (attemptedErrorClip) {
13703
+ errorClipOutcome = { status: "failed", reason: "buffer_empty" };
13704
+ }
13705
+ }
13706
+ const tags = applyReplayCaptureTags(
13707
+ {
13708
+ ...this.options.tags ?? {},
13709
+ ...args.context?.tags ?? {}
13710
+ },
13711
+ errorClipOutcome
13712
+ );
13713
+ const extra = mergeReplayCaptureExtra(
13714
+ args.context?.extra,
13715
+ errorClipOutcome
13716
+ );
13637
13717
  try {
13638
13718
  await ingestEvent(this.transport, {
13639
13719
  message: args.message,
@@ -13645,7 +13725,7 @@
13645
13725
  release: this.options.release,
13646
13726
  userId: args.context?.userId ?? this.options.userId,
13647
13727
  sessionId: this.sessionId ?? void 0,
13648
- replayId,
13728
+ replayId: replayId ?? void 0,
13649
13729
  url: typeof location !== "undefined" ? location.href : void 0,
13650
13730
  tags: Object.keys(tags).length ? tags : void 0,
13651
13731
  extraJson: extra ? JSON.stringify(extra) : void 0,
@@ -13790,9 +13870,10 @@
13790
13870
  /**
13791
13871
  * Ensure segment 0 begins at Meta+FullSnapshot. Orphan increments alone
13792
13872
  * produce a blank rrweb player.
13873
+ * @returns false when no FullSnapshot is available.
13793
13874
  */
13794
13875
  prepareBufferForNewReplay() {
13795
- if (this.segmentIndex !== 0) return;
13876
+ if (this.segmentIndex !== 0) return true;
13796
13877
  if (!this.buffer.hasFullSnapshot()) {
13797
13878
  this.checkoutFullSnapshot();
13798
13879
  }
@@ -13800,6 +13881,40 @@
13800
13881
  console.warn(
13801
13882
  "@newtalaria/browser: replay has no FullSnapshot; player may show a blank screen"
13802
13883
  );
13884
+ return false;
13885
+ }
13886
+ return true;
13887
+ }
13888
+ /**
13889
+ * Stop an error clip that cannot paint (missing/oversized FullSnapshot).
13890
+ * Does not set linkableReplayId — the error event should not open a blank player.
13891
+ */
13892
+ async abortUnusableClip(reason, details) {
13893
+ this.lastReplayCaptureFailure = { reason, details };
13894
+ console.warn("@newtalaria/browser: aborting unusable replay clip", {
13895
+ reason,
13896
+ ...details
13897
+ });
13898
+ this.buffer.clear();
13899
+ this.linkableReplayId = null;
13900
+ if (this.startedOnServer && !this.finishedOnServer && this.transport && this.replayId) {
13901
+ try {
13902
+ await finishReplay(this.transport, {
13903
+ replayId: this.replayId,
13904
+ reason: `capture_failed:${reason}`,
13905
+ keepalive: false
13906
+ });
13907
+ this.finishedOnServer = true;
13908
+ } catch (error) {
13909
+ console.warn("@newtalaria/browser: replays/finish failed after abort", error);
13910
+ }
13911
+ }
13912
+ if (!this.sessionSampled) {
13913
+ this.resetToBufferMode();
13914
+ } else {
13915
+ this.uploadEnabled = false;
13916
+ this.clearErrorClipTimer();
13917
+ this.clearMaxDurationTimer();
13803
13918
  }
13804
13919
  }
13805
13920
  stopUploadingAfterLimit() {
@@ -13818,6 +13933,7 @@
13818
13933
  installGlobalHandlers() {
13819
13934
  if (typeof window === "undefined") return;
13820
13935
  const onError = (event) => {
13936
+ if (isOpaqueCrossOriginScriptError(event)) return;
13821
13937
  const error = event.error instanceof Error ? event.error : new Error(event.message || "window.onerror");
13822
13938
  void this.captureException(error, {
13823
13939
  extra: {
@@ -13860,7 +13976,10 @@
13860
13976
  async uploadPendingSegments(opts) {
13861
13977
  if (!this.options || !this.transport || !this.replayId) return;
13862
13978
  if (!this.startedOnServer || this.finishedOnServer) return;
13863
- this.prepareBufferForNewReplay();
13979
+ if (!this.prepareBufferForNewReplay()) {
13980
+ await this.abortUnusableClip("no_full_snapshot");
13981
+ return;
13982
+ }
13864
13983
  while (this.buffer.length > 0) {
13865
13984
  if (this.isPastMaxDuration()) {
13866
13985
  break;
@@ -13900,7 +14019,17 @@
13900
14019
  }
13901
14020
  const fitted = await this.takeFittedSegment();
13902
14021
  if (!fitted) break;
14022
+ if (fitted.abortReason) {
14023
+ await this.abortUnusableClip(fitted.abortReason, fitted.abortDetails);
14024
+ return;
14025
+ }
13903
14026
  const { events, gzip } = fitted;
14027
+ if (this.segmentIndex === 0 && !events.some((e) => e.type === RRWEB_FULL_SNAPSHOT)) {
14028
+ await this.abortUnusableClip("no_full_snapshot", {
14029
+ segmentEventTypes: events.map((e) => e.type)
14030
+ });
14031
+ return;
14032
+ }
13904
14033
  if (this.isErrorClipMode() && this.uploadedCompressedBytes + gzip.length > MAX_ERROR_CLIP_COMPRESSED_BYTES && this.segmentIndex > 0) {
13905
14034
  this.buffer.clear();
13906
14035
  await this.finishOnServer({
@@ -13925,6 +14054,9 @@
13925
14054
  });
13926
14055
  this.segmentIndex = index2 + 1;
13927
14056
  this.uploadedCompressedBytes += gzip.length;
14057
+ if (index2 === 0) {
14058
+ this.lastReplayCaptureFailure = null;
14059
+ }
13928
14060
  } catch (error) {
13929
14061
  if (isTerminalReplayLimitError(error)) {
13930
14062
  console.warn(
@@ -13941,10 +14073,17 @@
13941
14073
  if (isOversizedSegmentError(error)) {
13942
14074
  const plan = planOversizedRetry(events);
13943
14075
  if (plan.action === "drop") {
14076
+ const droppedType = events[0]?.type;
13944
14077
  console.warn(
13945
14078
  "@newtalaria/browser: dropping rrweb event rejected as oversized",
13946
- { type: events[0]?.type }
14079
+ { type: droppedType }
13947
14080
  );
14081
+ if (this.segmentIndex === 0 && droppedType === RRWEB_FULL_SNAPSHOT) {
14082
+ await this.abortUnusableClip("oversized_full_snapshot", {
14083
+ source: "server_reject"
14084
+ });
14085
+ return;
14086
+ }
13948
14087
  continue;
13949
14088
  }
13950
14089
  this.buffer.prepend(plan.right);
@@ -13953,6 +14092,11 @@
13953
14092
  }
13954
14093
  this.buffer.prepend(events);
13955
14094
  console.warn("@newtalaria/browser: replays/ingestSegment failed", error);
14095
+ if (this.segmentIndex === 0) {
14096
+ await this.abortUnusableClip("upload_failed", {
14097
+ message: error instanceof Error ? error.message : String(error)
14098
+ });
14099
+ }
13956
14100
  break;
13957
14101
  }
13958
14102
  if (opts.keepalive) break;
@@ -13967,7 +14111,8 @@
13967
14111
  }
13968
14112
  /**
13969
14113
  * Pull the largest event prefix that gzips under the target compressed size.
13970
- * Drops a single event that can never fit.
14114
+ * Drops a single event that can never fit — except FullSnapshot on segment 0
14115
+ * (abort the clip instead of uploading a blank orphan stream).
13971
14116
  */
13972
14117
  async takeFittedSegment() {
13973
14118
  const chunk = this.buffer.takeByEstimatedBytes(SEGMENT_SIZE_BYTES);
@@ -13979,12 +14124,32 @@
13979
14124
  MAX_COMPRESSED_SEGMENT_BYTES
13980
14125
  );
13981
14126
  if (!fitted) {
14127
+ const dropped = chunk[0];
13982
14128
  console.warn(
13983
14129
  "@newtalaria/browser: dropping rrweb event that exceeds max segment size",
13984
- { type: chunk[0]?.type }
14130
+ { type: dropped?.type }
13985
14131
  );
14132
+ if (this.segmentIndex === 0 && dropped?.type === RRWEB_FULL_SNAPSHOT) {
14133
+ return {
14134
+ events: [],
14135
+ gzip: new Uint8Array(),
14136
+ abortReason: "oversized_full_snapshot",
14137
+ abortDetails: { source: "fit_compressed_prefix" }
14138
+ };
14139
+ }
14140
+ if (chunk.length > 1) {
14141
+ this.buffer.prepend(chunk.slice(1));
14142
+ }
13986
14143
  return this.takeFittedSegment();
13987
14144
  }
14145
+ if (this.segmentIndex === 0 && chunk.some((e) => e.type === RRWEB_FULL_SNAPSHOT) && !fitted.events.some((e) => e.type === RRWEB_FULL_SNAPSHOT)) {
14146
+ return {
14147
+ events: [],
14148
+ gzip: new Uint8Array(),
14149
+ abortReason: "oversized_full_snapshot",
14150
+ abortDetails: { source: "fit_dropped_full_snapshot" }
14151
+ };
14152
+ }
13988
14153
  if (fitted.remainder.length > 0) {
13989
14154
  this.buffer.prepend(fitted.remainder);
13990
14155
  }