@newtalaria/browser 0.1.9 → 0.1.12

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
@@ -53,7 +53,9 @@ await Talaria.flush();
53
53
  await Talaria.close();
54
54
  ```
55
55
 
56
- `Talaria.init` also installs `window.onerror` / `unhandledrejection` handlers unless you pass `disableDefaultIntegrations: true`. Opaque cross-origin `"Script error."` events (no usable `Error` object) are ignored by default.
56
+ `Talaria.init` also installs `window.onerror` / `unhandledrejection` handlers unless you pass `disableDefaultIntegrations: true`. Opaque cross-origin `"Script error."` events (no usable `Error` object) are ignored by default. Errors from browser extensions (`chrome-extension://`, `moz-extension://`, etc.) are also ignored.
57
+
58
+ Every ingested event includes browser runtime tags (`browser.name`, `browser.version`, `os.name`, `os.version`, `device`) and `extra.browser` / `extra.sdk` for triage.
57
59
 
58
60
  Optional init `tags` are merged into every captured event (per-call tags win on key conflict).
59
61
 
@@ -99,7 +101,7 @@ You pay for **uploaded + retained** bytes, not for local buffering. Prefer error
99
101
  | 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
102
  | Error sample + `replaysErrorAfterMs = 0` | Upload buffer then continue like session mode until **5 min** / unload / size caps |
101
103
  | Server limit (replay segments / total size / duration) | Stop uploading that replay; no retries |
102
- | Oversized FullSnapshot on segment 0 | **Abort** the clip (no blank orphan upload); error event gets `replay.capture=failed` + reason |
104
+ | Oversized FullSnapshot on segment 0 | **Abort** the clip (no blank orphan upload); error event gets `replay.capture=failed` + reason. Meta+FullSnapshot are taken/fitted **atomically** so a soft estimated take window cannot orphan the snapshot. |
103
105
  | Oversized single non-FS rrweb event | Dropped with a console warning (cannot fit under segment cap) |
104
106
  | `pagehide` / `close` | Flush pending segments with `fetch` `keepalive`, then `replays/finish`; `close` fully resets so `init()` works again (React Strict Mode) |
105
107
 
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { CaptureContext, SeverityLevel, TalariaInitOptions } from './types.js';
2
- export { computeErrorClipDeadlineMs, fitCompressedPrefix, isErrorClipBudgetExhausted, planOversizedRetry, } from './replay/fit_segment.js';
2
+ export { computeErrorClipDeadlineMs, fitCompressedPrefix, isErrorClipBudgetExhausted, paintBaseEventCount, planOversizedRetry, } from './replay/fit_segment.js';
3
3
  export { MAX_SEGMENTS_ERROR_CLIP, MAX_ERROR_CLIP_COMPRESSED_BYTES, TARGET_COMPRESSED_SEGMENT_BYTES, MAX_COMPRESSED_SEGMENT_BYTES, } from './replay/segment_buffer.js';
4
4
  export { REPLAY_CAPTURE_TAG, REPLAY_CAPTURE_REASON_TAG, applyReplayCaptureTags, mergeReplayCaptureExtra, } from './replay/capture_outcome.js';
5
5
  export type { ReplayCaptureOutcome, ReplayCaptureReason, ReplayCaptureStatus, } from './replay/capture_outcome.js';
@@ -34,6 +34,8 @@ export declare class TalariaClient {
34
34
  private linkableReplayId;
35
35
  /** Last error-clip paint/upload failure (cleared on successful segment 0). */
36
36
  private lastReplayCaptureFailure;
37
+ /** Cached once per init for event tags/extra. */
38
+ private browserContext;
37
39
  init(options: TalariaInitOptions): void;
38
40
  getReplayId(): string | null;
39
41
  captureException(error: unknown, context?: CaptureContext): Promise<void>;
@@ -89,8 +91,9 @@ export declare class TalariaClient {
89
91
  private maxSegmentsAllowed;
90
92
  /**
91
93
  * Pull the largest event prefix that gzips under the target compressed size.
92
- * Drops a single event that can never fit — except FullSnapshot on segment 0
93
- * (abort the clip instead of uploading a blank orphan stream).
94
+ *
95
+ * Segment 0 keeps Meta+FullSnapshot (paint base) atomic through take + fit so
96
+ * a soft estimated-byte window cannot orphan the FullSnapshot.
94
97
  */
95
98
  private takeFittedSegment;
96
99
  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;AA8CpB,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;AACpC,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,6BAA6B,CAAC;AACrC,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,6BAA6B,CAAC;AA4ErC,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;IAC/C,8EAA8E;IAC9E,OAAO,CAAC,wBAAwB,CAAqC;IAErE,IAAI,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI;IA8FvC,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;IAoD5B,OAAO,CAAC,YAAY;YAwBN,OAAO;IA4HrB,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;;;;OAIG;IACH,OAAO,CAAC,yBAAyB;IAcjC;;;OAGG;YACW,iBAAiB;IAkC/B,OAAO,CAAC,uBAAuB;IAc/B,OAAO,CAAC,qBAAqB;IAgC7B,OAAO,CAAC,aAAa;YAKP,aAAa;YAkBb,qBAAqB;IA0LnC,OAAO,CAAC,kBAAkB;IAQ1B;;;;OAIG;YACW,iBAAiB;YAoFjB,cAAc;CAuB7B"}
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;AAuDpB,OAAO,EACL,0BAA0B,EAC1B,mBAAmB,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,uBAAuB,EACvB,+BAA+B,EAC/B,+BAA+B,EAC/B,4BAA4B,GAC7B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,6BAA6B,CAAC;AACrC,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,6BAA6B,CAAC;AA4ErC,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;IAC/C,8EAA8E;IAC9E,OAAO,CAAC,wBAAwB,CAAqC;IACrE,iDAAiD;IACjD,OAAO,CAAC,cAAc,CAA+B;IAErD,IAAI,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI;IAkGvC,WAAW,IAAI,MAAM,GAAG,IAAI;IAKtB,gBAAgB,CACpB,KAAK,EAAE,OAAO,EACd,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC;IA6BV,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;IAqD5B,OAAO,CAAC,YAAY;YAwBN,OAAO;IA+IrB,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;;;;OAIG;IACH,OAAO,CAAC,yBAAyB;IAcjC;;;OAGG;YACW,iBAAiB;IAkC/B,OAAO,CAAC,uBAAuB;IAc/B,OAAO,CAAC,qBAAqB;IA2C7B,OAAO,CAAC,aAAa;YAKP,aAAa;YAkBb,qBAAqB;IAwLnC,OAAO,CAAC,kBAAkB;IAQ1B;;;;;OAKG;YACW,iBAAiB;YAkEjB,cAAc;CAuB7B"}
package/dist/index.js CHANGED
@@ -15,6 +15,21 @@ function createId() {
15
15
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
16
16
  }
17
17
 
18
+ // src/utils/browser_extension_noise.ts
19
+ var EXTENSION_URL = /(?:chrome|moz|safari|safari-web|ms-browser)-extension:\/\//i;
20
+ var WEBKIT_MASKED = /webkit-masked-url:\/\/hidden\//i;
21
+ function isBrowserExtensionNoise(opts) {
22
+ const filename = opts.filename ?? "";
23
+ if (EXTENSION_URL.test(filename) || WEBKIT_MASKED.test(filename)) {
24
+ return true;
25
+ }
26
+ const stack = opts.stack ?? "";
27
+ if (EXTENSION_URL.test(stack) || WEBKIT_MASKED.test(stack)) {
28
+ return true;
29
+ }
30
+ return false;
31
+ }
32
+
18
33
  // src/utils/opaque_script_error.ts
19
34
  function isOpaqueCrossOriginScriptError(event) {
20
35
  const msg = (event.message ?? "").trim();
@@ -23,6 +38,137 @@ function isOpaqueCrossOriginScriptError(event) {
23
38
  return true;
24
39
  }
25
40
 
41
+ // src/utils/browser_context.ts
42
+ function parseBrowserContext(ua = typeof navigator !== "undefined" ? navigator.userAgent : "", language = typeof navigator !== "undefined" ? navigator.language : "") {
43
+ const userAgent = ua || "";
44
+ const lang = language || "";
45
+ let name = "unknown";
46
+ let version = "";
47
+ const rules = [
48
+ { name: "Edge", re: /Edg(?:e|A|iOS)?\/([\d.]+)/ },
49
+ { name: "Opera", re: /OPR\/([\d.]+)/ },
50
+ { name: "Samsung Internet", re: /SamsungBrowser\/([\d.]+)/ },
51
+ { name: "Firefox", re: /Firefox\/([\d.]+)/ },
52
+ { name: "Chrome", re: /(?:Chrome|CriOS)\/([\d.]+)/ },
53
+ { name: "Safari", re: /Version\/([\d.]+).*Safari/ }
54
+ ];
55
+ for (const rule2 of rules) {
56
+ const m = userAgent.match(rule2.re);
57
+ if (m) {
58
+ name = rule2.name;
59
+ version = m[1] ?? "";
60
+ break;
61
+ }
62
+ }
63
+ if (name === "unknown" && /iPhone|iPad|iPod/.test(userAgent) && /AppleWebKit/.test(userAgent)) {
64
+ name = "Safari";
65
+ const m = userAgent.match(/OS ([\d_]+)/);
66
+ if (m?.[1]) version = m[1].replace(/_/g, ".");
67
+ }
68
+ let os = "unknown";
69
+ let osVersion = "";
70
+ const win = userAgent.match(/Windows NT ([\d.]+)/);
71
+ if (win) {
72
+ os = "Windows";
73
+ osVersion = win[1] ?? "";
74
+ } else {
75
+ const android = userAgent.match(/Android ([\d.]+)/);
76
+ if (android) {
77
+ os = "Android";
78
+ osVersion = android[1] ?? "";
79
+ } else {
80
+ const ios = userAgent.match(/(?:iPhone|iPad|iPod).*OS ([\d_]+)/);
81
+ if (ios) {
82
+ os = "iOS";
83
+ osVersion = (ios[1] ?? "").replace(/_/g, ".");
84
+ } else {
85
+ const mac = userAgent.match(/Mac OS X ([\d_]+)/);
86
+ if (mac) {
87
+ os = "macOS";
88
+ osVersion = (mac[1] ?? "").replace(/_/g, ".");
89
+ } else if (/CrOS/.test(userAgent)) {
90
+ os = "Chrome OS";
91
+ } else if (/Linux/.test(userAgent)) {
92
+ os = "Linux";
93
+ }
94
+ }
95
+ }
96
+ }
97
+ let device = "desktop";
98
+ if (/iPad|Tablet|Android(?!.*Mobile)/i.test(userAgent)) {
99
+ device = "tablet";
100
+ } else if (/Mobi|iPhone|iPod|Android.*Mobile/i.test(userAgent)) {
101
+ device = "mobile";
102
+ } else if (!userAgent) {
103
+ device = "unknown";
104
+ }
105
+ return {
106
+ name,
107
+ version,
108
+ os,
109
+ osVersion,
110
+ device,
111
+ language: lang,
112
+ userAgent
113
+ };
114
+ }
115
+ function browserContextTags(ctx) {
116
+ return {
117
+ "browser.name": ctx.name,
118
+ "browser.version": ctx.version || "unknown",
119
+ "os.name": ctx.os,
120
+ "os.version": ctx.osVersion || "unknown",
121
+ device: ctx.device
122
+ };
123
+ }
124
+ async function collectBrowserContext() {
125
+ const base = parseBrowserContext();
126
+ const nav = typeof navigator !== "undefined" ? navigator : void 0;
127
+ const uad = nav?.userAgentData;
128
+ if (!uad) return base;
129
+ if (uad.mobile === true) base.device = "mobile";
130
+ if (uad.platform) {
131
+ const p = uad.platform;
132
+ if (/Windows/i.test(p)) base.os = "Windows";
133
+ else if (/macOS|Mac OS/i.test(p)) base.os = "macOS";
134
+ else if (/Android/i.test(p)) base.os = "Android";
135
+ else if (/iOS|iPhone/i.test(p)) base.os = "iOS";
136
+ else if (/Linux/i.test(p)) base.os = "Linux";
137
+ else if (/Chrome OS|Chromium OS/i.test(p)) base.os = "Chrome OS";
138
+ else base.os = p;
139
+ }
140
+ const brands = uad.brands ?? [];
141
+ const interesting = brands.find(
142
+ (b) => !/Not.?A.?Brand/i.test(b.brand) && !/Chromium/i.test(b.brand)
143
+ );
144
+ if (interesting) {
145
+ base.name = interesting.brand.replace(/^Google /, "");
146
+ base.version = interesting.version;
147
+ }
148
+ try {
149
+ if (typeof uad.getHighEntropyValues === "function") {
150
+ const hi = await uad.getHighEntropyValues([
151
+ "platformVersion",
152
+ "fullVersionList"
153
+ ]);
154
+ if (hi.platformVersion) base.osVersion = hi.platformVersion;
155
+ const full = hi.fullVersionList?.find(
156
+ (b) => !/Not.?A.?Brand/i.test(b.brand) && !/Chromium/i.test(b.brand)
157
+ );
158
+ if (full) {
159
+ base.name = full.brand.replace(/^Google /, "");
160
+ base.version = full.version;
161
+ }
162
+ }
163
+ } catch {
164
+ }
165
+ return base;
166
+ }
167
+
168
+ // src/sdk_meta.ts
169
+ var SDK_NAME = "@newtalaria/browser";
170
+ var SDK_VERSION = "0.1.12";
171
+
26
172
  // src/transport/serverpod.ts
27
173
  var ServerpodTransport = class {
28
174
  constructor(options) {
@@ -204,15 +350,44 @@ var SegmentBuffer = class {
204
350
  shouldFlushBySize() {
205
351
  return this.pendingBytes >= SEGMENT_SIZE_BYTES;
206
352
  }
353
+ /**
354
+ * How many leading events form the rrweb paint base (Meta+FullSnapshot or
355
+ * FullSnapshot alone). Returns 0 if the buffer does not start with a paint base.
356
+ */
357
+ paintBaseLength() {
358
+ if (this.events.length === 0) return 0;
359
+ if (this.events[0].type === RRWEB_META && this.events[1]?.type === RRWEB_FULL_SNAPSHOT) {
360
+ return 2;
361
+ }
362
+ if (this.events[0].type === RRWEB_FULL_SNAPSHOT) {
363
+ return 1;
364
+ }
365
+ return 0;
366
+ }
207
367
  /**
208
368
  * Take a prefix of events whose estimated JSON size is ~maxBytes.
209
369
  * Always returns at least one event when the buffer is non-empty (caller
210
370
  * must handle a single oversized event).
371
+ *
372
+ * When `keepPaintBaseTogether` is true (segment 0), Meta immediately followed
373
+ * by FullSnapshot is taken as one unit even if estimated size exceeds
374
+ * maxBytes — gzip fitting / hard caps decide whether it can ship.
211
375
  */
212
- takeByEstimatedBytes(maxBytes) {
376
+ takeByEstimatedBytes(maxBytes, opts) {
213
377
  if (this.events.length === 0) return [];
214
378
  const batch = [];
215
379
  let batchBytes = 0;
380
+ if (opts?.keepPaintBaseTogether) {
381
+ const paintLen = this.paintBaseLength();
382
+ if (paintLen > 0) {
383
+ for (let i = 0; i < paintLen; i++) {
384
+ const ev = this.events.shift();
385
+ batch.push(ev);
386
+ batchBytes += estimateJsonBytes(ev);
387
+ }
388
+ this.recomputeBytes();
389
+ }
390
+ }
216
391
  while (this.events.length > 0) {
217
392
  const next = this.events[0];
218
393
  const nextBytes = estimateJsonBytes(next);
@@ -337,8 +512,53 @@ function planOversizedRetry(events) {
337
512
  right: events.slice(mid)
338
513
  };
339
514
  }
340
- async function fitCompressedPrefix(events, compress, targetBytes = TARGET_COMPRESSED_SEGMENT_BYTES, hardCap = MAX_COMPRESSED_SEGMENT_BYTES) {
515
+ function paintBaseEventCount(events) {
516
+ if (events.length === 0) return 0;
517
+ if (events[0].type === RRWEB_META && events[1]?.type === RRWEB_FULL_SNAPSHOT) {
518
+ return 2;
519
+ }
520
+ if (events[0].type === RRWEB_FULL_SNAPSHOT) {
521
+ return 1;
522
+ }
523
+ return 0;
524
+ }
525
+ async function fitCompressedPrefix(events, compress, targetBytes = TARGET_COMPRESSED_SEGMENT_BYTES, hardCap = MAX_COMPRESSED_SEGMENT_BYTES, opts) {
341
526
  if (events.length === 0) return null;
527
+ const atomicMin = opts?.atomicMinCount ?? 0;
528
+ if (atomicMin > 0) {
529
+ if (events.length < atomicMin) return null;
530
+ const paint = events.slice(0, atomicMin);
531
+ const paintGzip = await compress(paint);
532
+ if (paintGzip.length > hardCap) {
533
+ return null;
534
+ }
535
+ let lo = atomicMin;
536
+ let hi = events.length;
537
+ let best = {
538
+ events: paint,
539
+ gzip: paintGzip
540
+ };
541
+ while (lo <= hi) {
542
+ const mid = Math.floor((lo + hi) / 2);
543
+ if (mid < atomicMin) {
544
+ lo = atomicMin;
545
+ continue;
546
+ }
547
+ const prefix = events.slice(0, mid);
548
+ const gzip = await compress(prefix);
549
+ if (gzip.length <= targetBytes && gzip.length <= hardCap) {
550
+ best = { events: prefix, gzip };
551
+ lo = mid + 1;
552
+ } else {
553
+ hi = mid - 1;
554
+ }
555
+ }
556
+ return {
557
+ events: best.events,
558
+ gzip: best.gzip,
559
+ remainder: events.slice(best.events.length)
560
+ };
561
+ }
342
562
  let remaining = events;
343
563
  while (remaining.length > 0) {
344
564
  let lo = 1;
@@ -400,7 +620,7 @@ function paintBaseSizeDetails(events, opts) {
400
620
  const full = events.find((e) => e.type === RRWEB_FULL_SNAPSHOT);
401
621
  const details = {
402
622
  segmentEventTypes: events.map((e) => e.type),
403
- maxUncompressedSegmentBytes: SEGMENT_SIZE_BYTES,
623
+ softTakeEstimatedBytes: SEGMENT_SIZE_BYTES,
404
624
  targetCompressedSegmentBytes: TARGET_COMPRESSED_SEGMENT_BYTES,
405
625
  maxCompressedSegmentBytes: MAX_COMPRESSED_SEGMENT_BYTES
406
626
  };
@@ -13462,6 +13682,8 @@ var TalariaClient = class {
13462
13682
  this.linkableReplayId = null;
13463
13683
  /** Last error-clip paint/upload failure (cleared on successful segment 0). */
13464
13684
  this.lastReplayCaptureFailure = null;
13685
+ /** Cached once per init for event tags/extra. */
13686
+ this.browserContext = null;
13465
13687
  }
13466
13688
  init(options) {
13467
13689
  if (this.options) {
@@ -13484,6 +13706,10 @@ var TalariaClient = class {
13484
13706
  this.errorClipDeadlineMs = null;
13485
13707
  this.linkableReplayId = null;
13486
13708
  this.lastReplayCaptureFailure = null;
13709
+ this.browserContext = parseBrowserContext();
13710
+ void collectBrowserContext().then((ctx) => {
13711
+ if (this.options) this.browserContext = ctx;
13712
+ });
13487
13713
  this.buffer = new SegmentBuffer();
13488
13714
  this.uploadChain = Promise.resolve();
13489
13715
  this.sessionSampled = Math.random() < this.options.replaysSessionSampleRate;
@@ -13553,6 +13779,14 @@ var TalariaClient = class {
13553
13779
  }
13554
13780
  async captureException(error, context) {
13555
13781
  const err = error instanceof Error ? error : new Error(typeof error === "string" ? error : "Unknown error");
13782
+ const filename = typeof context?.extra?.filename === "string" ? context.extra.filename : void 0;
13783
+ if (isBrowserExtensionNoise({
13784
+ message: err.message,
13785
+ stack: err.stack,
13786
+ filename
13787
+ })) {
13788
+ return;
13789
+ }
13556
13790
  await this.capture({
13557
13791
  message: err.message || err.name || "Error",
13558
13792
  level: "error",
@@ -13646,6 +13880,7 @@ var TalariaClient = class {
13646
13880
  this.closed = false;
13647
13881
  this.linkableReplayId = null;
13648
13882
  this.lastReplayCaptureFailure = null;
13883
+ this.browserContext = null;
13649
13884
  }
13650
13885
  onRrwebEvent(event) {
13651
13886
  if (this.closed || !this.options) return;
@@ -13729,13 +13964,28 @@ var TalariaClient = class {
13729
13964
  }
13730
13965
  const tags = applyReplayCaptureTags(
13731
13966
  {
13967
+ ...this.browserContext ? browserContextTags(this.browserContext) : {},
13732
13968
  ...this.options.tags ?? {},
13733
13969
  ...args.context?.tags ?? {}
13734
13970
  },
13735
13971
  errorClipOutcome
13736
13972
  );
13737
13973
  const extra = mergeReplayCaptureExtra(
13738
- args.context?.extra,
13974
+ {
13975
+ ...this.browserContext ? {
13976
+ browser: {
13977
+ name: this.browserContext.name,
13978
+ version: this.browserContext.version,
13979
+ os: this.browserContext.os,
13980
+ osVersion: this.browserContext.osVersion,
13981
+ device: this.browserContext.device,
13982
+ language: this.browserContext.language,
13983
+ userAgent: this.browserContext.userAgent
13984
+ }
13985
+ } : {},
13986
+ sdk: { name: SDK_NAME, version: SDK_VERSION },
13987
+ ...args.context?.extra
13988
+ },
13739
13989
  errorClipOutcome
13740
13990
  );
13741
13991
  try {
@@ -13959,6 +14209,13 @@ var TalariaClient = class {
13959
14209
  const onError = (event) => {
13960
14210
  if (isOpaqueCrossOriginScriptError(event)) return;
13961
14211
  const error = event.error instanceof Error ? event.error : new Error(event.message || "window.onerror");
14212
+ if (isBrowserExtensionNoise({
14213
+ message: event.message || error.message,
14214
+ stack: error.stack,
14215
+ filename: event.filename
14216
+ })) {
14217
+ return;
14218
+ }
13962
14219
  void this.captureException(error, {
13963
14220
  extra: {
13964
14221
  filename: event.filename,
@@ -14053,20 +14310,9 @@ var TalariaClient = class {
14053
14310
  }
14054
14311
  const { events, gzip } = fitted;
14055
14312
  if (this.segmentIndex === 0 && !events.some((e) => e.type === RRWEB_FULL_SNAPSHOT)) {
14056
- const leftoverFs = this.buffer.peekFullSnapshot();
14057
- let compressedBytes;
14058
- if (leftoverFs) {
14059
- try {
14060
- compressedBytes = (await compressReplayEvents([leftoverFs])).length;
14061
- } catch {
14062
- }
14063
- }
14064
14313
  await this.abortUnusableClip(
14065
14314
  "no_full_snapshot",
14066
- paintBaseSizeDetails(
14067
- leftoverFs ? [...events, leftoverFs] : events,
14068
- { fullSnapshotCompressedBytes: compressedBytes }
14069
- )
14315
+ paintBaseSizeDetails(events)
14070
14316
  );
14071
14317
  return;
14072
14318
  }
@@ -14111,6 +14357,14 @@ var TalariaClient = class {
14111
14357
  break;
14112
14358
  }
14113
14359
  if (isOversizedSegmentError(error)) {
14360
+ if (this.segmentIndex === 0 && paintBaseEventCount(events) > 0) {
14361
+ await this.abortUnusableClip("oversized_full_snapshot", {
14362
+ source: "server_reject_paint_base",
14363
+ ...paintBaseSizeDetails(events),
14364
+ rejectedCompressedBytes: gzip.length
14365
+ });
14366
+ return;
14367
+ }
14114
14368
  const plan = planOversizedRetry(events);
14115
14369
  if (plan.action === "drop") {
14116
14370
  const droppedType = events[0]?.type;
@@ -14118,7 +14372,7 @@ var TalariaClient = class {
14118
14372
  "@newtalaria/browser: dropping rrweb event rejected as oversized",
14119
14373
  { type: droppedType }
14120
14374
  );
14121
- if (this.segmentIndex === 0 && droppedType === RRWEB_FULL_SNAPSHOT) {
14375
+ if (droppedType === RRWEB_FULL_SNAPSHOT) {
14122
14376
  await this.abortUnusableClip(
14123
14377
  "oversized_full_snapshot",
14124
14378
  {
@@ -14156,68 +14410,60 @@ var TalariaClient = class {
14156
14410
  }
14157
14411
  /**
14158
14412
  * Pull the largest event prefix that gzips under the target compressed size.
14159
- * Drops a single event that can never fit — except FullSnapshot on segment 0
14160
- * (abort the clip instead of uploading a blank orphan stream).
14413
+ *
14414
+ * Segment 0 keeps Meta+FullSnapshot (paint base) atomic through take + fit so
14415
+ * a soft estimated-byte window cannot orphan the FullSnapshot.
14161
14416
  */
14162
14417
  async takeFittedSegment() {
14163
- const chunk = this.buffer.takeByEstimatedBytes(SEGMENT_SIZE_BYTES);
14418
+ const isPaintBaseSegment = this.segmentIndex === 0;
14419
+ const chunk = this.buffer.takeByEstimatedBytes(SEGMENT_SIZE_BYTES, {
14420
+ keepPaintBaseTogether: isPaintBaseSegment
14421
+ });
14164
14422
  if (chunk.length === 0) return null;
14423
+ const atomicMin = isPaintBaseSegment ? paintBaseEventCount(chunk) : 0;
14165
14424
  const fitted = await fitCompressedPrefix(
14166
14425
  chunk,
14167
14426
  compressReplayEvents,
14168
14427
  TARGET_COMPRESSED_SEGMENT_BYTES,
14169
- MAX_COMPRESSED_SEGMENT_BYTES
14428
+ MAX_COMPRESSED_SEGMENT_BYTES,
14429
+ atomicMin > 0 ? { atomicMinCount: atomicMin } : void 0
14170
14430
  );
14171
14431
  if (!fitted) {
14172
- const dropped = chunk[0];
14173
- console.warn(
14174
- "@newtalaria/browser: dropping rrweb event that exceeds max segment size",
14175
- { type: dropped?.type }
14176
- );
14177
- if (this.segmentIndex === 0 && dropped?.type === RRWEB_FULL_SNAPSHOT) {
14432
+ if (isPaintBaseSegment && atomicMin > 0) {
14433
+ const paint = chunk.slice(0, atomicMin);
14178
14434
  let compressedBytes;
14179
- try {
14180
- compressedBytes = (await compressReplayEvents([dropped])).length;
14181
- } catch {
14435
+ const full = paint.find((e) => e.type === RRWEB_FULL_SNAPSHOT);
14436
+ if (full) {
14437
+ try {
14438
+ compressedBytes = (await compressReplayEvents([full])).length;
14439
+ } catch {
14440
+ }
14441
+ }
14442
+ if (chunk.length > atomicMin) {
14443
+ this.buffer.prepend(chunk.slice(atomicMin));
14182
14444
  }
14183
14445
  return {
14184
14446
  events: [],
14185
14447
  gzip: new Uint8Array(),
14186
14448
  abortReason: "oversized_full_snapshot",
14187
14449
  abortDetails: {
14188
- source: "fit_compressed_prefix",
14189
- ...paintBaseSizeDetails(chunk, {
14450
+ source: "paint_base_exceeds_hard_cap",
14451
+ ...paintBaseSizeDetails(paint, {
14190
14452
  fullSnapshotCompressedBytes: compressedBytes
14191
14453
  })
14192
14454
  }
14193
14455
  };
14194
14456
  }
14457
+ const dropped = chunk[0];
14458
+ console.warn(
14459
+ "@newtalaria/browser: dropping rrweb event that exceeds max segment size",
14460
+ { type: dropped?.type }
14461
+ );
14195
14462
  if (chunk.length > 1) {
14196
14463
  this.buffer.prepend(chunk.slice(1));
14197
14464
  }
14198
14465
  return this.takeFittedSegment();
14199
14466
  }
14200
- if (this.segmentIndex === 0 && chunk.some((e) => e.type === RRWEB_FULL_SNAPSHOT) && !fitted.events.some((e) => e.type === RRWEB_FULL_SNAPSHOT)) {
14201
- const full = chunk.find((e) => e.type === RRWEB_FULL_SNAPSHOT);
14202
- let compressedBytes;
14203
- if (full) {
14204
- try {
14205
- compressedBytes = (await compressReplayEvents([full])).length;
14206
- } catch {
14207
- }
14208
- }
14209
- return {
14210
- events: [],
14211
- gzip: new Uint8Array(),
14212
- abortReason: "oversized_full_snapshot",
14213
- abortDetails: {
14214
- source: "fit_dropped_full_snapshot",
14215
- ...paintBaseSizeDetails(chunk, {
14216
- fullSnapshotCompressedBytes: compressedBytes
14217
- })
14218
- }
14219
- };
14220
- }
14221
14467
  if (fitted.remainder.length > 0) {
14222
14468
  this.buffer.prepend(fitted.remainder);
14223
14469
  }