@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.
package/README.md CHANGED
@@ -96,12 +96,24 @@ You pay for **uploaded + retained** bytes, not for local buffering. Prefer error
96
96
  | --- | --- |
97
97
  | Session sample hit | `replays/start` immediately; segments upload every ~5s or ~100KB until unload or **5 min** max |
98
98
  | Session sample miss | Record into a ~60s ring buffer; nothing uploaded until an error sample hits |
99
- | Error sample + `replaysErrorAfterMs > 0` | Upload buffer (≤480KiB gzip pack target / 512KiB server cap) + trailing window, hard caps **12 segments** or **2MiB** compressed, attach `replayId` only if segments landed, `finish`, return to buffer mode |
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 single rrweb event | Dropped with a console warning (cannot fit under segment cap) |
102
+ | Oversized FullSnapshot on segment 0 | **Abort** the clip (no blank orphan upload); error event gets `replay.capture=failed` + reason |
103
+ | Oversized single non-FS rrweb event | Dropped with a console warning (cannot fit under segment cap) |
103
104
  | `pagehide` / `close` | Flush pending segments with `fetch` `keepalive`, then `replays/finish`; `close` fully resets so `init()` works again (React Strict Mode) |
104
105
 
106
+ ### Replay capture outcome tags
107
+
108
+ When an **error/fatal** event attempts an on-error clip (`replaysSessionSampleRate` miss), the SDK may attach:
109
+
110
+ | Tag | Values |
111
+ | --- | --- |
112
+ | `replay.capture` | `ok` \| `failed` \| `skipped` |
113
+ | `replay.capture_reason` | `oversized_full_snapshot` \| `no_full_snapshot` \| `upload_failed` \| `not_sampled` \| `buffer_empty` |
114
+
115
+ Failed captures do **not** set `replayId` (avoids linking a blank player). Details are also under `extra.replayCapture`.
116
+
105
117
  ## Serverpod URL pattern
106
118
 
107
119
  Talaria uses **Serverpod RPC**, not REST resource URLs:
@@ -157,6 +169,26 @@ By default `inlineStylesheet` is `false`: linked stylesheets stay as `href`s and
157
169
 
158
170
  Set `inlineStylesheet: true` so same-origin stylesheet rules are embedded while the user is logged in. Cross-origin sheets without CORS still cannot be inlined (browser `cssRules` restriction).
159
171
 
172
+ ### Failed HTTP requests → events
173
+
174
+ By default, completed `fetch` / XHR responses with status **500–599** are also sent as Talaria events (message like `HTTP 500: GET /api/...`). Replay still gets `talaria-network` breadcrumbs for all requests.
175
+
176
+ ```ts
177
+ Talaria.init({
178
+ dsn: 'https://api.example.com',
179
+ apiKey: 'tal_live_…',
180
+ environment: 'production',
181
+ // Default true
182
+ captureFailedRequests: true,
183
+ // Default [[500, 599]]. CMS admin often wants 4xx too:
184
+ failedRequestStatusCodes: [[400, 599]],
185
+ // Extra URL substrings to skip (Talaria /events and /replays are always skipped)
186
+ failedRequestIgnoreUrls: ['/health'],
187
+ });
188
+ ```
189
+
190
+ Transport errors (network down, no status) are not promoted as HTTP events; uncaught exceptions from failed fetches still go through normal `error` / `unhandledrejection` handlers when the app rethrows.
191
+
160
192
  ## Public API
161
193
 
162
194
  | API | Description |
package/dist/client.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { CaptureContext, SeverityLevel, TalariaInitOptions } from './types.js';
2
2
  export { computeErrorClipDeadlineMs, fitCompressedPrefix, isErrorClipBudgetExhausted, 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
+ export { REPLAY_CAPTURE_TAG, REPLAY_CAPTURE_REASON_TAG, applyReplayCaptureTags, mergeReplayCaptureExtra, } from './replay/capture_outcome.js';
5
+ export type { ReplayCaptureOutcome, ReplayCaptureReason, ReplayCaptureStatus, } from './replay/capture_outcome.js';
4
6
  export declare class TalariaClient {
5
7
  private options;
6
8
  private transport;
@@ -30,6 +32,8 @@ export declare class TalariaClient {
30
32
  * attach it to the error event after budget/timer finish resets buffer mode.
31
33
  */
32
34
  private linkableReplayId;
35
+ /** Last error-clip paint/upload failure (cleared on successful segment 0). */
36
+ private lastReplayCaptureFailure;
33
37
  init(options: TalariaInitOptions): void;
34
38
  getReplayId(): string | null;
35
39
  captureException(error: unknown, context?: CaptureContext): Promise<void>;
@@ -69,8 +73,14 @@ export declare class TalariaClient {
69
73
  /**
70
74
  * Ensure segment 0 begins at Meta+FullSnapshot. Orphan increments alone
71
75
  * produce a blank rrweb player.
76
+ * @returns false when no FullSnapshot is available.
72
77
  */
73
78
  private prepareBufferForNewReplay;
79
+ /**
80
+ * Stop an error clip that cannot paint (missing/oversized FullSnapshot).
81
+ * Does not set linkableReplayId — the error event should not open a blank player.
82
+ */
83
+ private abortUnusableClip;
74
84
  private stopUploadingAfterLimit;
75
85
  private installGlobalHandlers;
76
86
  private enqueueUpload;
@@ -79,7 +89,8 @@ export declare class TalariaClient {
79
89
  private maxSegmentsAllowed;
80
90
  /**
81
91
  * Pull the largest event prefix that gzips under the target compressed size.
82
- * Drops a single event that can never fit.
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).
83
94
  */
84
95
  private takeFittedSegment;
85
96
  private finishOnServer;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EAEd,aAAa,EACb,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAoCpB,OAAO,EACL,0BAA0B,EAC1B,mBAAmB,EACnB,0BAA0B,EAC1B,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,uBAAuB,EACvB,+BAA+B,EAC/B,+BAA+B,EAC/B,4BAA4B,GAC7B,MAAM,4BAA4B,CAAC;AAyEpC,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,SAAS,CAAmC;IACpD,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,MAAM,CAAuB;IACrC,yEAAyE;IACzE,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,uBAAuB,CAAK;IACpC,0EAA0E;IAC1E,OAAO,CAAC,iBAAiB,CAAuB;IAChD,4EAA4E;IAC5E,OAAO,CAAC,mBAAmB,CAAuB;IAClD,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,gBAAgB,CAA8C;IACtE,OAAO,CAAC,WAAW,CAAoC;IACvD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAkB;IACnC;;;OAGG;IACH,OAAO,CAAC,gBAAgB,CAAuB;IAE/C,IAAI,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI;IAiEvC,WAAW,IAAI,MAAM,GAAG,IAAI;IAKtB,gBAAgB,CACpB,KAAK,EAAE,OAAO,EACd,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC;IAcV,cAAc,CAClB,OAAO,EAAE,MAAM,EACf,KAAK,GAAE,aAAsB,EAC7B,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC;IASV,KAAK,CAAC,IAAI,CAAC,EAAE;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB,MAAM,CAAC,EAAE,OAAO,CAAC;KAClB,GAAG,OAAO,CAAC,IAAI,CAAC;IA0CjB;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAmD5B,OAAO,CAAC,YAAY;YAwBN,OAAO;IA+ErB,OAAO,CAAC,iBAAiB;IAkBzB,OAAO,CAAC,iBAAiB;IAKzB,OAAO,CAAC,uBAAuB;IAK/B,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,uBAAuB;IAiB/B,OAAO,CAAC,qBAAqB;YAOf,qBAAqB;IAsBnC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAmB5B,OAAO,CAAC,mBAAmB;IAO3B,8EAA8E;YAChE,YAAY;YAgBZ,eAAe;IAW7B,oFAAoF;IACpF,OAAO,CAAC,iBAAiB;IAiBzB,+EAA+E;IAC/E,OAAO,CAAC,oBAAoB;IAI5B;;;OAGG;IACH,OAAO,CAAC,yBAAyB;IAYjC,OAAO,CAAC,uBAAuB;IAc/B,OAAO,CAAC,qBAAqB;IA6B7B,OAAO,CAAC,aAAa;YAKP,aAAa;YAkBb,qBAAqB;IA8HnC,OAAO,CAAC,kBAAkB;IAQ1B;;;OAGG;YACW,iBAAiB;YA2BjB,cAAc;CAuB7B"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -32,6 +32,8 @@ export declare const Talaria: {
32
32
  export { TalariaClient } from './client.js';
33
33
  export { computeErrorClipDeadlineMs, fitCompressedPrefix, isErrorClipBudgetExhausted, planOversizedRetry, } from './replay/fit_segment.js';
34
34
  export { MAX_SEGMENTS_ERROR_CLIP, MAX_ERROR_CLIP_COMPRESSED_BYTES, TARGET_COMPRESSED_SEGMENT_BYTES, MAX_COMPRESSED_SEGMENT_BYTES, } from './replay/segment_buffer.js';
35
- export type { CaptureContext, Environment, SeverityLevel, TalariaInitOptions, } from './types.js';
35
+ export type { CaptureContext, Environment, FailedRequestStatusCode, SeverityLevel, TalariaInitOptions, } from './types.js';
36
+ export { REPLAY_CAPTURE_TAG, REPLAY_CAPTURE_REASON_TAG, } from './replay/capture_outcome.js';
37
+ export type { ReplayCaptureOutcome, ReplayCaptureReason, ReplayCaptureStatus, } from './replay/capture_outcome.js';
36
38
  export default Talaria;
37
39
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAIpB;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,OAAO;kBACJ,kBAAkB,GAAG,IAAI;4BAIf,OAAO,YAAY,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;4BAK9D,MAAM,UACP,aAAa,YACX,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC;mBAID,MAAM,GAAG,IAAI;aAInB,OAAO,CAAC,IAAI,CAAC;aAIb,OAAO,CAAC,IAAI,CAAC;CAGvB,CAAC;AAEF,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EACL,0BAA0B,EAC1B,mBAAmB,EACnB,0BAA0B,EAC1B,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,uBAAuB,EACvB,+BAA+B,EAC/B,+BAA+B,EAC/B,4BAA4B,GAC7B,MAAM,4BAA4B,CAAC;AAEpC,YAAY,EACV,cAAc,EACd,WAAW,EACX,aAAa,EACb,kBAAkB,GACnB,MAAM,YAAY,CAAC;AAEpB,eAAe,OAAO,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAIpB;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,OAAO;kBACJ,kBAAkB,GAAG,IAAI;4BAIf,OAAO,YAAY,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;4BAK9D,MAAM,UACP,aAAa,YACX,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC;mBAID,MAAM,GAAG,IAAI;aAInB,OAAO,CAAC,IAAI,CAAC;aAIb,OAAO,CAAC,IAAI,CAAC;CAGvB,CAAC;AAEF,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EACL,0BAA0B,EAC1B,mBAAmB,EACnB,0BAA0B,EAC1B,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,uBAAuB,EACvB,+BAA+B,EAC/B,+BAA+B,EAC/B,4BAA4B,GAC7B,MAAM,4BAA4B,CAAC;AAEpC,YAAY,EACV,cAAc,EACd,WAAW,EACX,uBAAuB,EACvB,aAAa,EACb,kBAAkB,GACnB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,kBAAkB,EAClB,yBAAyB,GAC1B,MAAM,6BAA6B,CAAC;AACrC,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,6BAA6B,CAAC;AAErC,eAAe,OAAO,CAAC"}
package/dist/index.js CHANGED
@@ -121,9 +121,9 @@ function estimateJsonBytes(value) {
121
121
  }
122
122
 
123
123
  // src/replay/segment_buffer.ts
124
- var SEGMENT_SIZE_BYTES = 4e5;
125
- var MAX_COMPRESSED_SEGMENT_BYTES = 512 * 1024;
126
- var TARGET_COMPRESSED_SEGMENT_BYTES = MAX_COMPRESSED_SEGMENT_BYTES - 32 * 1024;
124
+ var SEGMENT_SIZE_BYTES = 8e5;
125
+ var MAX_COMPRESSED_SEGMENT_BYTES = 1024 * 1024;
126
+ var TARGET_COMPRESSED_SEGMENT_BYTES = MAX_COMPRESSED_SEGMENT_BYTES - 64 * 1024;
127
127
  var MAX_SEGMENTS_PER_REPLAY = 200;
128
128
  var MAX_SEGMENTS_ERROR_CLIP = 12;
129
129
  var MAX_ERROR_CLIP_COMPRESSED_BYTES = 2 * 1024 * 1024;
@@ -355,6 +355,33 @@ async function fitCompressedPrefix(events, compress, targetBytes = TARGET_COMPRE
355
355
  return null;
356
356
  }
357
357
 
358
+ // src/replay/capture_outcome.ts
359
+ var REPLAY_CAPTURE_TAG = "replay.capture";
360
+ var REPLAY_CAPTURE_REASON_TAG = "replay.capture_reason";
361
+ function applyReplayCaptureTags(tags, outcome) {
362
+ if (!outcome) return tags;
363
+ const next = { ...tags };
364
+ next[REPLAY_CAPTURE_TAG] = outcome.status;
365
+ if (outcome.reason) {
366
+ next[REPLAY_CAPTURE_REASON_TAG] = outcome.reason;
367
+ }
368
+ return next;
369
+ }
370
+ function mergeReplayCaptureExtra(extra, outcome) {
371
+ if (!outcome || outcome.status === "ok") {
372
+ return extra;
373
+ }
374
+ return {
375
+ ...extra ?? {},
376
+ replayCapture: {
377
+ attempted: outcome.status === "failed",
378
+ status: outcome.status,
379
+ reason: outcome.reason,
380
+ ...outcome.details ?? {}
381
+ }
382
+ };
383
+ }
384
+
358
385
  // ../../node_modules/rrweb/dist/rrweb.js
359
386
  var __defProp = Object.defineProperty;
360
387
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -13214,11 +13241,52 @@ function emitNetwork(meta) {
13214
13241
  } catch {
13215
13242
  }
13216
13243
  }
13217
- function installNetworkHook() {
13244
+ function statusMatches(status, codes) {
13245
+ for (const entry of codes) {
13246
+ if (typeof entry === "number") {
13247
+ if (status === entry) return true;
13248
+ } else if (Array.isArray(entry) && entry.length >= 2) {
13249
+ const [min, max] = entry;
13250
+ if (status >= min && status <= max) return true;
13251
+ }
13252
+ }
13253
+ return false;
13254
+ }
13255
+ function shouldPromoteFailedRequest(meta, opts) {
13256
+ if (!opts.captureFailedRequests) return false;
13257
+ if (typeof meta.status !== "number" || Number.isNaN(meta.status)) return false;
13258
+ if (!statusMatches(meta.status, opts.failedRequestStatusCodes)) return false;
13259
+ const url = meta.url || "";
13260
+ const ignore = [...opts.failedRequestIgnoreUrls];
13261
+ const base = (opts.talariaBaseUrl ?? "").replace(/\/+$/, "");
13262
+ if (base) {
13263
+ ignore.push(`${base}/events/`, `${base}/replays/`);
13264
+ }
13265
+ ignore.push("/events/ingest", "/events/ingestBatch", "/replays/");
13266
+ const lower = url.toLowerCase();
13267
+ for (const part of ignore) {
13268
+ if (part && lower.includes(part.toLowerCase())) return false;
13269
+ }
13270
+ return true;
13271
+ }
13272
+ function installNetworkHook(options = {}) {
13218
13273
  const originalFetch = typeof fetch === "function" ? fetch.bind(globalThis) : null;
13219
13274
  const XHR = typeof XMLHttpRequest !== "undefined" ? XMLHttpRequest : null;
13220
13275
  const originalOpen = XHR?.prototype.open;
13221
13276
  const originalSend = XHR?.prototype.send;
13277
+ const matchOpts = {
13278
+ captureFailedRequests: options.captureFailedRequests ?? true,
13279
+ failedRequestStatusCodes: options.failedRequestStatusCodes ?? [[500, 599]],
13280
+ failedRequestIgnoreUrls: options.failedRequestIgnoreUrls ?? [],
13281
+ talariaBaseUrl: options.talariaBaseUrl
13282
+ };
13283
+ const handleMeta = (meta) => {
13284
+ emitNetwork(meta);
13285
+ options.onNetwork?.(meta);
13286
+ if (shouldPromoteFailedRequest(meta, matchOpts)) {
13287
+ options.onFailedRequest?.(meta);
13288
+ }
13289
+ };
13222
13290
  if (originalFetch) {
13223
13291
  globalThis.fetch = async (input2, init) => {
13224
13292
  const started = Date.now();
@@ -13226,7 +13294,7 @@ function installNetworkHook() {
13226
13294
  const url = typeof input2 === "string" ? input2 : input2 instanceof URL ? input2.toString() : input2.url;
13227
13295
  try {
13228
13296
  const response = await originalFetch(input2, init);
13229
- emitNetwork({
13297
+ handleMeta({
13230
13298
  method,
13231
13299
  url,
13232
13300
  status: response.status,
@@ -13235,7 +13303,7 @@ function installNetworkHook() {
13235
13303
  });
13236
13304
  return response;
13237
13305
  } catch (error) {
13238
- emitNetwork({
13306
+ handleMeta({
13239
13307
  method,
13240
13308
  url,
13241
13309
  durationMs: Date.now() - started,
@@ -13254,7 +13322,7 @@ function installNetworkHook() {
13254
13322
  XHR.prototype.send = function(body) {
13255
13323
  this.__talariaStarted = Date.now();
13256
13324
  const onDone = () => {
13257
- emitNetwork({
13325
+ handleMeta({
13258
13326
  method: this.__talariaMethod ?? "GET",
13259
13327
  url: this.__talariaUrl ?? "",
13260
13328
  status: this.status,
@@ -13298,7 +13366,10 @@ function resolveOptions(options) {
13298
13366
  blockSelector: options.blockSelector ?? "",
13299
13367
  userId: options.userId,
13300
13368
  tags: options.tags,
13301
- disableDefaultIntegrations: options.disableDefaultIntegrations ?? false
13369
+ disableDefaultIntegrations: options.disableDefaultIntegrations ?? false,
13370
+ captureFailedRequests: options.captureFailedRequests ?? true,
13371
+ failedRequestStatusCodes: options.failedRequestStatusCodes ?? [[500, 599]],
13372
+ failedRequestIgnoreUrls: options.failedRequestIgnoreUrls ?? []
13302
13373
  };
13303
13374
  }
13304
13375
  function clamp01(n2) {
@@ -13355,6 +13426,8 @@ var TalariaClient = class {
13355
13426
  * attach it to the error event after budget/timer finish resets buffer mode.
13356
13427
  */
13357
13428
  this.linkableReplayId = null;
13429
+ /** Last error-clip paint/upload failure (cleared on successful segment 0). */
13430
+ this.lastReplayCaptureFailure = null;
13358
13431
  }
13359
13432
  init(options) {
13360
13433
  if (this.options) {
@@ -13376,6 +13449,7 @@ var TalariaClient = class {
13376
13449
  this.uploadStartedAtMs = null;
13377
13450
  this.errorClipDeadlineMs = null;
13378
13451
  this.linkableReplayId = null;
13452
+ this.lastReplayCaptureFailure = null;
13379
13453
  this.buffer = new SegmentBuffer();
13380
13454
  this.uploadChain = Promise.resolve();
13381
13455
  this.sessionSampled = Math.random() < this.options.replaysSessionSampleRate;
@@ -13386,7 +13460,35 @@ var TalariaClient = class {
13386
13460
  blockSelector: this.options.blockSelector,
13387
13461
  onEvent: (event) => this.onRrwebEvent(event)
13388
13462
  });
13389
- this.teardowns.push(installConsoleHook(), installNetworkHook());
13463
+ this.teardowns.push(
13464
+ installConsoleHook(),
13465
+ installNetworkHook({
13466
+ captureFailedRequests: this.options.captureFailedRequests,
13467
+ failedRequestStatusCodes: this.options.failedRequestStatusCodes,
13468
+ failedRequestIgnoreUrls: this.options.failedRequestIgnoreUrls,
13469
+ talariaBaseUrl: this.options.baseUrl,
13470
+ onFailedRequest: (meta) => {
13471
+ const status = meta.status;
13472
+ const method = meta.method || "GET";
13473
+ const url = meta.url || "(unknown url)";
13474
+ void this.captureMessage(
13475
+ `HTTP ${status}: ${method} ${url}`,
13476
+ status >= 500 ? "error" : "warning",
13477
+ {
13478
+ tags: {
13479
+ "http.status_code": String(status),
13480
+ "http.method": method
13481
+ },
13482
+ extra: {
13483
+ url,
13484
+ durationMs: meta.durationMs,
13485
+ ok: meta.ok
13486
+ }
13487
+ }
13488
+ );
13489
+ }
13490
+ })
13491
+ );
13390
13492
  if (!this.options.disableDefaultIntegrations) {
13391
13493
  this.installGlobalHandlers();
13392
13494
  }
@@ -13509,6 +13611,7 @@ var TalariaClient = class {
13509
13611
  this.uploadChain = Promise.resolve();
13510
13612
  this.closed = false;
13511
13613
  this.linkableReplayId = null;
13614
+ this.lastReplayCaptureFailure = null;
13512
13615
  }
13513
13616
  onRrwebEvent(event) {
13514
13617
  if (this.closed || !this.options) return;
@@ -13534,15 +13637,22 @@ var TalariaClient = class {
13534
13637
  throw new Error("@newtalaria/browser: call Talaria.init() first");
13535
13638
  }
13536
13639
  const isErrorLike = args.level === "error" || args.level === "fatal";
13640
+ let errorClipOutcome = null;
13641
+ let attemptedErrorClip = false;
13537
13642
  if (isErrorLike && !this.sessionSampled) {
13538
13643
  if (!this.uploadEnabled) {
13539
13644
  if (Math.random() < this.options.replaysOnErrorSampleRate) {
13645
+ attemptedErrorClip = true;
13646
+ this.lastReplayCaptureFailure = null;
13540
13647
  this.uploadEnabled = true;
13541
13648
  this.checkoutFullSnapshot();
13542
13649
  this.markUploadStarted();
13543
13650
  this.scheduleErrorClipEnd();
13651
+ } else {
13652
+ errorClipOutcome = { status: "skipped", reason: "not_sampled" };
13544
13653
  }
13545
13654
  } else {
13655
+ attemptedErrorClip = true;
13546
13656
  this.scheduleErrorClipEnd();
13547
13657
  }
13548
13658
  }
@@ -13551,15 +13661,49 @@ var TalariaClient = class {
13551
13661
  await this.flush({ reason: "capture", keepalive: args.keepalive });
13552
13662
  } catch (error) {
13553
13663
  console.warn("@newtalaria/browser: replay flush failed", error);
13554
- throw error;
13664
+ this.lastReplayCaptureFailure = {
13665
+ reason: "upload_failed",
13666
+ details: {
13667
+ message: error instanceof Error ? error.message : String(error)
13668
+ }
13669
+ };
13670
+ if (!this.sessionSampled && this.segmentIndex === 0) {
13671
+ await this.abortUnusableClip("upload_failed", {
13672
+ message: error instanceof Error ? error.message : String(error)
13673
+ });
13674
+ }
13555
13675
  }
13556
13676
  }
13557
- const tags = {
13558
- ...this.options.tags ?? {},
13559
- ...args.context?.tags ?? {}
13560
- };
13561
- const extra = args.context?.extra;
13562
- const replayId = this.uploadEnabled && this.segmentIndex > 0 ? this.replayId : this.linkableReplayId;
13677
+ let replayId = this.uploadEnabled && this.segmentIndex > 0 ? this.replayId : this.linkableReplayId;
13678
+ if (this.lastReplayCaptureFailure && this.segmentIndex === 0) {
13679
+ replayId = null;
13680
+ }
13681
+ if (isErrorLike && !this.sessionSampled) {
13682
+ if (replayId) {
13683
+ errorClipOutcome = { status: "ok" };
13684
+ this.lastReplayCaptureFailure = null;
13685
+ } else if (errorClipOutcome?.status === "skipped") {
13686
+ } else if (this.lastReplayCaptureFailure) {
13687
+ errorClipOutcome = {
13688
+ status: "failed",
13689
+ reason: this.lastReplayCaptureFailure.reason,
13690
+ details: this.lastReplayCaptureFailure.details
13691
+ };
13692
+ } else if (attemptedErrorClip) {
13693
+ errorClipOutcome = { status: "failed", reason: "buffer_empty" };
13694
+ }
13695
+ }
13696
+ const tags = applyReplayCaptureTags(
13697
+ {
13698
+ ...this.options.tags ?? {},
13699
+ ...args.context?.tags ?? {}
13700
+ },
13701
+ errorClipOutcome
13702
+ );
13703
+ const extra = mergeReplayCaptureExtra(
13704
+ args.context?.extra,
13705
+ errorClipOutcome
13706
+ );
13563
13707
  try {
13564
13708
  await ingestEvent(this.transport, {
13565
13709
  message: args.message,
@@ -13571,7 +13715,7 @@ var TalariaClient = class {
13571
13715
  release: this.options.release,
13572
13716
  userId: args.context?.userId ?? this.options.userId,
13573
13717
  sessionId: this.sessionId ?? void 0,
13574
- replayId,
13718
+ replayId: replayId ?? void 0,
13575
13719
  url: typeof location !== "undefined" ? location.href : void 0,
13576
13720
  tags: Object.keys(tags).length ? tags : void 0,
13577
13721
  extraJson: extra ? JSON.stringify(extra) : void 0,
@@ -13716,9 +13860,10 @@ var TalariaClient = class {
13716
13860
  /**
13717
13861
  * Ensure segment 0 begins at Meta+FullSnapshot. Orphan increments alone
13718
13862
  * produce a blank rrweb player.
13863
+ * @returns false when no FullSnapshot is available.
13719
13864
  */
13720
13865
  prepareBufferForNewReplay() {
13721
- if (this.segmentIndex !== 0) return;
13866
+ if (this.segmentIndex !== 0) return true;
13722
13867
  if (!this.buffer.hasFullSnapshot()) {
13723
13868
  this.checkoutFullSnapshot();
13724
13869
  }
@@ -13726,6 +13871,40 @@ var TalariaClient = class {
13726
13871
  console.warn(
13727
13872
  "@newtalaria/browser: replay has no FullSnapshot; player may show a blank screen"
13728
13873
  );
13874
+ return false;
13875
+ }
13876
+ return true;
13877
+ }
13878
+ /**
13879
+ * Stop an error clip that cannot paint (missing/oversized FullSnapshot).
13880
+ * Does not set linkableReplayId — the error event should not open a blank player.
13881
+ */
13882
+ async abortUnusableClip(reason, details) {
13883
+ this.lastReplayCaptureFailure = { reason, details };
13884
+ console.warn("@newtalaria/browser: aborting unusable replay clip", {
13885
+ reason,
13886
+ ...details
13887
+ });
13888
+ this.buffer.clear();
13889
+ this.linkableReplayId = null;
13890
+ if (this.startedOnServer && !this.finishedOnServer && this.transport && this.replayId) {
13891
+ try {
13892
+ await finishReplay(this.transport, {
13893
+ replayId: this.replayId,
13894
+ reason: `capture_failed:${reason}`,
13895
+ keepalive: false
13896
+ });
13897
+ this.finishedOnServer = true;
13898
+ } catch (error) {
13899
+ console.warn("@newtalaria/browser: replays/finish failed after abort", error);
13900
+ }
13901
+ }
13902
+ if (!this.sessionSampled) {
13903
+ this.resetToBufferMode();
13904
+ } else {
13905
+ this.uploadEnabled = false;
13906
+ this.clearErrorClipTimer();
13907
+ this.clearMaxDurationTimer();
13729
13908
  }
13730
13909
  }
13731
13910
  stopUploadingAfterLimit() {
@@ -13786,7 +13965,10 @@ var TalariaClient = class {
13786
13965
  async uploadPendingSegments(opts) {
13787
13966
  if (!this.options || !this.transport || !this.replayId) return;
13788
13967
  if (!this.startedOnServer || this.finishedOnServer) return;
13789
- this.prepareBufferForNewReplay();
13968
+ if (!this.prepareBufferForNewReplay()) {
13969
+ await this.abortUnusableClip("no_full_snapshot");
13970
+ return;
13971
+ }
13790
13972
  while (this.buffer.length > 0) {
13791
13973
  if (this.isPastMaxDuration()) {
13792
13974
  break;
@@ -13826,7 +14008,17 @@ var TalariaClient = class {
13826
14008
  }
13827
14009
  const fitted = await this.takeFittedSegment();
13828
14010
  if (!fitted) break;
14011
+ if (fitted.abortReason) {
14012
+ await this.abortUnusableClip(fitted.abortReason, fitted.abortDetails);
14013
+ return;
14014
+ }
13829
14015
  const { events, gzip } = fitted;
14016
+ if (this.segmentIndex === 0 && !events.some((e) => e.type === RRWEB_FULL_SNAPSHOT)) {
14017
+ await this.abortUnusableClip("no_full_snapshot", {
14018
+ segmentEventTypes: events.map((e) => e.type)
14019
+ });
14020
+ return;
14021
+ }
13830
14022
  if (this.isErrorClipMode() && this.uploadedCompressedBytes + gzip.length > MAX_ERROR_CLIP_COMPRESSED_BYTES && this.segmentIndex > 0) {
13831
14023
  this.buffer.clear();
13832
14024
  await this.finishOnServer({
@@ -13851,6 +14043,9 @@ var TalariaClient = class {
13851
14043
  });
13852
14044
  this.segmentIndex = index2 + 1;
13853
14045
  this.uploadedCompressedBytes += gzip.length;
14046
+ if (index2 === 0) {
14047
+ this.lastReplayCaptureFailure = null;
14048
+ }
13854
14049
  } catch (error) {
13855
14050
  if (isTerminalReplayLimitError(error)) {
13856
14051
  console.warn(
@@ -13867,10 +14062,17 @@ var TalariaClient = class {
13867
14062
  if (isOversizedSegmentError(error)) {
13868
14063
  const plan = planOversizedRetry(events);
13869
14064
  if (plan.action === "drop") {
14065
+ const droppedType = events[0]?.type;
13870
14066
  console.warn(
13871
14067
  "@newtalaria/browser: dropping rrweb event rejected as oversized",
13872
- { type: events[0]?.type }
14068
+ { type: droppedType }
13873
14069
  );
14070
+ if (this.segmentIndex === 0 && droppedType === RRWEB_FULL_SNAPSHOT) {
14071
+ await this.abortUnusableClip("oversized_full_snapshot", {
14072
+ source: "server_reject"
14073
+ });
14074
+ return;
14075
+ }
13874
14076
  continue;
13875
14077
  }
13876
14078
  this.buffer.prepend(plan.right);
@@ -13879,6 +14081,11 @@ var TalariaClient = class {
13879
14081
  }
13880
14082
  this.buffer.prepend(events);
13881
14083
  console.warn("@newtalaria/browser: replays/ingestSegment failed", error);
14084
+ if (this.segmentIndex === 0) {
14085
+ await this.abortUnusableClip("upload_failed", {
14086
+ message: error instanceof Error ? error.message : String(error)
14087
+ });
14088
+ }
13882
14089
  break;
13883
14090
  }
13884
14091
  if (opts.keepalive) break;
@@ -13893,7 +14100,8 @@ var TalariaClient = class {
13893
14100
  }
13894
14101
  /**
13895
14102
  * Pull the largest event prefix that gzips under the target compressed size.
13896
- * Drops a single event that can never fit.
14103
+ * Drops a single event that can never fit — except FullSnapshot on segment 0
14104
+ * (abort the clip instead of uploading a blank orphan stream).
13897
14105
  */
13898
14106
  async takeFittedSegment() {
13899
14107
  const chunk = this.buffer.takeByEstimatedBytes(SEGMENT_SIZE_BYTES);
@@ -13905,12 +14113,32 @@ var TalariaClient = class {
13905
14113
  MAX_COMPRESSED_SEGMENT_BYTES
13906
14114
  );
13907
14115
  if (!fitted) {
14116
+ const dropped = chunk[0];
13908
14117
  console.warn(
13909
14118
  "@newtalaria/browser: dropping rrweb event that exceeds max segment size",
13910
- { type: chunk[0]?.type }
14119
+ { type: dropped?.type }
13911
14120
  );
14121
+ if (this.segmentIndex === 0 && dropped?.type === RRWEB_FULL_SNAPSHOT) {
14122
+ return {
14123
+ events: [],
14124
+ gzip: new Uint8Array(),
14125
+ abortReason: "oversized_full_snapshot",
14126
+ abortDetails: { source: "fit_compressed_prefix" }
14127
+ };
14128
+ }
14129
+ if (chunk.length > 1) {
14130
+ this.buffer.prepend(chunk.slice(1));
14131
+ }
13912
14132
  return this.takeFittedSegment();
13913
14133
  }
14134
+ if (this.segmentIndex === 0 && chunk.some((e) => e.type === RRWEB_FULL_SNAPSHOT) && !fitted.events.some((e) => e.type === RRWEB_FULL_SNAPSHOT)) {
14135
+ return {
14136
+ events: [],
14137
+ gzip: new Uint8Array(),
14138
+ abortReason: "oversized_full_snapshot",
14139
+ abortDetails: { source: "fit_dropped_full_snapshot" }
14140
+ };
14141
+ }
13914
14142
  if (fitted.remainder.length > 0) {
13915
14143
  this.buffer.prepend(fitted.remainder);
13916
14144
  }
@@ -13962,6 +14190,8 @@ export {
13962
14190
  MAX_COMPRESSED_SEGMENT_BYTES,
13963
14191
  MAX_ERROR_CLIP_COMPRESSED_BYTES,
13964
14192
  MAX_SEGMENTS_ERROR_CLIP,
14193
+ REPLAY_CAPTURE_REASON_TAG,
14194
+ REPLAY_CAPTURE_TAG,
13965
14195
  TARGET_COMPRESSED_SEGMENT_BYTES,
13966
14196
  Talaria,
13967
14197
  TalariaClient,