@newtalaria/browser 0.1.5 → 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"}
@@ -1,6 +1,39 @@
1
+ import type { FailedRequestStatusCode } from '../types.js';
1
2
  export type Teardown = () => void;
3
+ export interface NetworkMeta {
4
+ method: string;
5
+ url: string;
6
+ status?: number;
7
+ durationMs?: number;
8
+ ok?: boolean;
9
+ }
10
+ export interface NetworkHookOptions {
11
+ /** Still emit rrweb breadcrumbs for all requests. */
12
+ onNetwork?: (meta: NetworkMeta) => void;
13
+ /**
14
+ * Called when a completed request should become a Talaria event.
15
+ * Network transport errors (no status) are not promoted here — they rethrow as usual.
16
+ */
17
+ onFailedRequest?: (meta: NetworkMeta & {
18
+ status: number;
19
+ }) => void;
20
+ captureFailedRequests?: boolean;
21
+ failedRequestStatusCodes?: FailedRequestStatusCode[];
22
+ failedRequestIgnoreUrls?: string[];
23
+ /** SDK base URL — requests under this host + /events|/replays are never promoted. */
24
+ talariaBaseUrl?: string;
25
+ }
2
26
  /** Mirror console output into rrweb as `talaria-console` custom events. */
3
27
  export declare function installConsoleHook(): Teardown;
4
- /** Capture fetch / XHR metadata only (no bodies, no auth headers). */
5
- export declare function installNetworkHook(): Teardown;
28
+ export declare function statusMatches(status: number, codes: FailedRequestStatusCode[]): boolean;
29
+ export declare function shouldPromoteFailedRequest(meta: NetworkMeta, opts: {
30
+ captureFailedRequests: boolean;
31
+ failedRequestStatusCodes: FailedRequestStatusCode[];
32
+ failedRequestIgnoreUrls: string[];
33
+ talariaBaseUrl?: string;
34
+ }): meta is NetworkMeta & {
35
+ status: number;
36
+ };
37
+ /** Capture fetch / XHR metadata (no bodies, no auth headers); optionally promote failures. */
38
+ export declare function installNetworkHook(options?: NetworkHookOptions): Teardown;
6
39
  //# sourceMappingURL=hooks.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/replay/hooks.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC;AAiBlC,2EAA2E;AAC3E,wBAAgB,kBAAkB,IAAI,QAAQ,CA2B7C;AAsBD,sEAAsE;AACtE,wBAAgB,kBAAkB,IAAI,QAAQ,CAgF7C"}
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/replay/hooks.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAE3D,MAAM,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC;AAElC,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,EAAE,CAAC,EAAE,OAAO,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,qDAAqD;IACrD,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC;IACxC;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,GAAG;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACnE,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,wBAAwB,CAAC,EAAE,uBAAuB,EAAE,CAAC;IACrD,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC,qFAAqF;IACrF,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAiBD,2EAA2E;AAC3E,wBAAgB,kBAAkB,IAAI,QAAQ,CA2B7C;AAcD,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,uBAAuB,EAAE,GAC/B,OAAO,CAUT;AAED,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,WAAW,EACjB,IAAI,EAAE;IACJ,qBAAqB,EAAE,OAAO,CAAC;IAC/B,wBAAwB,EAAE,uBAAuB,EAAE,CAAC;IACpD,uBAAuB,EAAE,MAAM,EAAE,CAAC;IAClC,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GACA,IAAI,IAAI,WAAW,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAoB1C;AAED,8FAA8F;AAC9F,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,kBAAuB,GAAG,QAAQ,CA+F7E"}
@@ -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;
@@ -13216,11 +13243,52 @@
13216
13243
  } catch {
13217
13244
  }
13218
13245
  }
13219
- function installNetworkHook() {
13246
+ function statusMatches(status, codes) {
13247
+ for (const entry of codes) {
13248
+ if (typeof entry === "number") {
13249
+ if (status === entry) return true;
13250
+ } else if (Array.isArray(entry) && entry.length >= 2) {
13251
+ const [min, max] = entry;
13252
+ if (status >= min && status <= max) return true;
13253
+ }
13254
+ }
13255
+ return false;
13256
+ }
13257
+ function shouldPromoteFailedRequest(meta, opts) {
13258
+ if (!opts.captureFailedRequests) return false;
13259
+ if (typeof meta.status !== "number" || Number.isNaN(meta.status)) return false;
13260
+ if (!statusMatches(meta.status, opts.failedRequestStatusCodes)) return false;
13261
+ const url = meta.url || "";
13262
+ const ignore = [...opts.failedRequestIgnoreUrls];
13263
+ const base = (opts.talariaBaseUrl ?? "").replace(/\/+$/, "");
13264
+ if (base) {
13265
+ ignore.push(`${base}/events/`, `${base}/replays/`);
13266
+ }
13267
+ ignore.push("/events/ingest", "/events/ingestBatch", "/replays/");
13268
+ const lower = url.toLowerCase();
13269
+ for (const part of ignore) {
13270
+ if (part && lower.includes(part.toLowerCase())) return false;
13271
+ }
13272
+ return true;
13273
+ }
13274
+ function installNetworkHook(options = {}) {
13220
13275
  const originalFetch = typeof fetch === "function" ? fetch.bind(globalThis) : null;
13221
13276
  const XHR = typeof XMLHttpRequest !== "undefined" ? XMLHttpRequest : null;
13222
13277
  const originalOpen = XHR?.prototype.open;
13223
13278
  const originalSend = XHR?.prototype.send;
13279
+ const matchOpts = {
13280
+ captureFailedRequests: options.captureFailedRequests ?? true,
13281
+ failedRequestStatusCodes: options.failedRequestStatusCodes ?? [[500, 599]],
13282
+ failedRequestIgnoreUrls: options.failedRequestIgnoreUrls ?? [],
13283
+ talariaBaseUrl: options.talariaBaseUrl
13284
+ };
13285
+ const handleMeta = (meta) => {
13286
+ emitNetwork(meta);
13287
+ options.onNetwork?.(meta);
13288
+ if (shouldPromoteFailedRequest(meta, matchOpts)) {
13289
+ options.onFailedRequest?.(meta);
13290
+ }
13291
+ };
13224
13292
  if (originalFetch) {
13225
13293
  globalThis.fetch = async (input2, init) => {
13226
13294
  const started = Date.now();
@@ -13228,7 +13296,7 @@
13228
13296
  const url = typeof input2 === "string" ? input2 : input2 instanceof URL ? input2.toString() : input2.url;
13229
13297
  try {
13230
13298
  const response = await originalFetch(input2, init);
13231
- emitNetwork({
13299
+ handleMeta({
13232
13300
  method,
13233
13301
  url,
13234
13302
  status: response.status,
@@ -13237,7 +13305,7 @@
13237
13305
  });
13238
13306
  return response;
13239
13307
  } catch (error) {
13240
- emitNetwork({
13308
+ handleMeta({
13241
13309
  method,
13242
13310
  url,
13243
13311
  durationMs: Date.now() - started,
@@ -13256,7 +13324,7 @@
13256
13324
  XHR.prototype.send = function(body) {
13257
13325
  this.__talariaStarted = Date.now();
13258
13326
  const onDone = () => {
13259
- emitNetwork({
13327
+ handleMeta({
13260
13328
  method: this.__talariaMethod ?? "GET",
13261
13329
  url: this.__talariaUrl ?? "",
13262
13330
  status: this.status,
@@ -13300,7 +13368,10 @@
13300
13368
  blockSelector: options.blockSelector ?? "",
13301
13369
  userId: options.userId,
13302
13370
  tags: options.tags,
13303
- disableDefaultIntegrations: options.disableDefaultIntegrations ?? false
13371
+ disableDefaultIntegrations: options.disableDefaultIntegrations ?? false,
13372
+ captureFailedRequests: options.captureFailedRequests ?? true,
13373
+ failedRequestStatusCodes: options.failedRequestStatusCodes ?? [[500, 599]],
13374
+ failedRequestIgnoreUrls: options.failedRequestIgnoreUrls ?? []
13304
13375
  };
13305
13376
  }
13306
13377
  function clamp01(n2) {
@@ -13357,6 +13428,8 @@
13357
13428
  * attach it to the error event after budget/timer finish resets buffer mode.
13358
13429
  */
13359
13430
  this.linkableReplayId = null;
13431
+ /** Last error-clip paint/upload failure (cleared on successful segment 0). */
13432
+ this.lastReplayCaptureFailure = null;
13360
13433
  }
13361
13434
  init(options) {
13362
13435
  if (this.options) {
@@ -13378,6 +13451,7 @@
13378
13451
  this.uploadStartedAtMs = null;
13379
13452
  this.errorClipDeadlineMs = null;
13380
13453
  this.linkableReplayId = null;
13454
+ this.lastReplayCaptureFailure = null;
13381
13455
  this.buffer = new SegmentBuffer();
13382
13456
  this.uploadChain = Promise.resolve();
13383
13457
  this.sessionSampled = Math.random() < this.options.replaysSessionSampleRate;
@@ -13388,7 +13462,35 @@
13388
13462
  blockSelector: this.options.blockSelector,
13389
13463
  onEvent: (event) => this.onRrwebEvent(event)
13390
13464
  });
13391
- this.teardowns.push(installConsoleHook(), installNetworkHook());
13465
+ this.teardowns.push(
13466
+ installConsoleHook(),
13467
+ installNetworkHook({
13468
+ captureFailedRequests: this.options.captureFailedRequests,
13469
+ failedRequestStatusCodes: this.options.failedRequestStatusCodes,
13470
+ failedRequestIgnoreUrls: this.options.failedRequestIgnoreUrls,
13471
+ talariaBaseUrl: this.options.baseUrl,
13472
+ onFailedRequest: (meta) => {
13473
+ const status = meta.status;
13474
+ const method = meta.method || "GET";
13475
+ const url = meta.url || "(unknown url)";
13476
+ void this.captureMessage(
13477
+ `HTTP ${status}: ${method} ${url}`,
13478
+ status >= 500 ? "error" : "warning",
13479
+ {
13480
+ tags: {
13481
+ "http.status_code": String(status),
13482
+ "http.method": method
13483
+ },
13484
+ extra: {
13485
+ url,
13486
+ durationMs: meta.durationMs,
13487
+ ok: meta.ok
13488
+ }
13489
+ }
13490
+ );
13491
+ }
13492
+ })
13493
+ );
13392
13494
  if (!this.options.disableDefaultIntegrations) {
13393
13495
  this.installGlobalHandlers();
13394
13496
  }
@@ -13511,6 +13613,7 @@
13511
13613
  this.uploadChain = Promise.resolve();
13512
13614
  this.closed = false;
13513
13615
  this.linkableReplayId = null;
13616
+ this.lastReplayCaptureFailure = null;
13514
13617
  }
13515
13618
  onRrwebEvent(event) {
13516
13619
  if (this.closed || !this.options) return;
@@ -13536,15 +13639,22 @@
13536
13639
  throw new Error("@newtalaria/browser: call Talaria.init() first");
13537
13640
  }
13538
13641
  const isErrorLike = args.level === "error" || args.level === "fatal";
13642
+ let errorClipOutcome = null;
13643
+ let attemptedErrorClip = false;
13539
13644
  if (isErrorLike && !this.sessionSampled) {
13540
13645
  if (!this.uploadEnabled) {
13541
13646
  if (Math.random() < this.options.replaysOnErrorSampleRate) {
13647
+ attemptedErrorClip = true;
13648
+ this.lastReplayCaptureFailure = null;
13542
13649
  this.uploadEnabled = true;
13543
13650
  this.checkoutFullSnapshot();
13544
13651
  this.markUploadStarted();
13545
13652
  this.scheduleErrorClipEnd();
13653
+ } else {
13654
+ errorClipOutcome = { status: "skipped", reason: "not_sampled" };
13546
13655
  }
13547
13656
  } else {
13657
+ attemptedErrorClip = true;
13548
13658
  this.scheduleErrorClipEnd();
13549
13659
  }
13550
13660
  }
@@ -13553,15 +13663,49 @@
13553
13663
  await this.flush({ reason: "capture", keepalive: args.keepalive });
13554
13664
  } catch (error) {
13555
13665
  console.warn("@newtalaria/browser: replay flush failed", error);
13556
- 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
+ }
13557
13677
  }
13558
13678
  }
13559
- const tags = {
13560
- ...this.options.tags ?? {},
13561
- ...args.context?.tags ?? {}
13562
- };
13563
- const extra = args.context?.extra;
13564
- 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
+ );
13565
13709
  try {
13566
13710
  await ingestEvent(this.transport, {
13567
13711
  message: args.message,
@@ -13573,7 +13717,7 @@
13573
13717
  release: this.options.release,
13574
13718
  userId: args.context?.userId ?? this.options.userId,
13575
13719
  sessionId: this.sessionId ?? void 0,
13576
- replayId,
13720
+ replayId: replayId ?? void 0,
13577
13721
  url: typeof location !== "undefined" ? location.href : void 0,
13578
13722
  tags: Object.keys(tags).length ? tags : void 0,
13579
13723
  extraJson: extra ? JSON.stringify(extra) : void 0,
@@ -13718,9 +13862,10 @@
13718
13862
  /**
13719
13863
  * Ensure segment 0 begins at Meta+FullSnapshot. Orphan increments alone
13720
13864
  * produce a blank rrweb player.
13865
+ * @returns false when no FullSnapshot is available.
13721
13866
  */
13722
13867
  prepareBufferForNewReplay() {
13723
- if (this.segmentIndex !== 0) return;
13868
+ if (this.segmentIndex !== 0) return true;
13724
13869
  if (!this.buffer.hasFullSnapshot()) {
13725
13870
  this.checkoutFullSnapshot();
13726
13871
  }
@@ -13728,6 +13873,40 @@
13728
13873
  console.warn(
13729
13874
  "@newtalaria/browser: replay has no FullSnapshot; player may show a blank screen"
13730
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();
13731
13910
  }
13732
13911
  }
13733
13912
  stopUploadingAfterLimit() {
@@ -13788,7 +13967,10 @@
13788
13967
  async uploadPendingSegments(opts) {
13789
13968
  if (!this.options || !this.transport || !this.replayId) return;
13790
13969
  if (!this.startedOnServer || this.finishedOnServer) return;
13791
- this.prepareBufferForNewReplay();
13970
+ if (!this.prepareBufferForNewReplay()) {
13971
+ await this.abortUnusableClip("no_full_snapshot");
13972
+ return;
13973
+ }
13792
13974
  while (this.buffer.length > 0) {
13793
13975
  if (this.isPastMaxDuration()) {
13794
13976
  break;
@@ -13828,7 +14010,17 @@
13828
14010
  }
13829
14011
  const fitted = await this.takeFittedSegment();
13830
14012
  if (!fitted) break;
14013
+ if (fitted.abortReason) {
14014
+ await this.abortUnusableClip(fitted.abortReason, fitted.abortDetails);
14015
+ return;
14016
+ }
13831
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
+ }
13832
14024
  if (this.isErrorClipMode() && this.uploadedCompressedBytes + gzip.length > MAX_ERROR_CLIP_COMPRESSED_BYTES && this.segmentIndex > 0) {
13833
14025
  this.buffer.clear();
13834
14026
  await this.finishOnServer({
@@ -13853,6 +14045,9 @@
13853
14045
  });
13854
14046
  this.segmentIndex = index2 + 1;
13855
14047
  this.uploadedCompressedBytes += gzip.length;
14048
+ if (index2 === 0) {
14049
+ this.lastReplayCaptureFailure = null;
14050
+ }
13856
14051
  } catch (error) {
13857
14052
  if (isTerminalReplayLimitError(error)) {
13858
14053
  console.warn(
@@ -13869,10 +14064,17 @@
13869
14064
  if (isOversizedSegmentError(error)) {
13870
14065
  const plan = planOversizedRetry(events);
13871
14066
  if (plan.action === "drop") {
14067
+ const droppedType = events[0]?.type;
13872
14068
  console.warn(
13873
14069
  "@newtalaria/browser: dropping rrweb event rejected as oversized",
13874
- { type: events[0]?.type }
14070
+ { type: droppedType }
13875
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
+ }
13876
14078
  continue;
13877
14079
  }
13878
14080
  this.buffer.prepend(plan.right);
@@ -13881,6 +14083,11 @@
13881
14083
  }
13882
14084
  this.buffer.prepend(events);
13883
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
+ }
13884
14091
  break;
13885
14092
  }
13886
14093
  if (opts.keepalive) break;
@@ -13895,7 +14102,8 @@
13895
14102
  }
13896
14103
  /**
13897
14104
  * Pull the largest event prefix that gzips under the target compressed size.
13898
- * 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).
13899
14107
  */
13900
14108
  async takeFittedSegment() {
13901
14109
  const chunk = this.buffer.takeByEstimatedBytes(SEGMENT_SIZE_BYTES);
@@ -13907,12 +14115,32 @@
13907
14115
  MAX_COMPRESSED_SEGMENT_BYTES
13908
14116
  );
13909
14117
  if (!fitted) {
14118
+ const dropped = chunk[0];
13910
14119
  console.warn(
13911
14120
  "@newtalaria/browser: dropping rrweb event that exceeds max segment size",
13912
- { type: chunk[0]?.type }
14121
+ { type: dropped?.type }
13913
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
+ }
13914
14134
  return this.takeFittedSegment();
13915
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
+ }
13916
14144
  if (fitted.remainder.length > 0) {
13917
14145
  this.buffer.prepend(fitted.remainder);
13918
14146
  }