@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.
@@ -23,11 +23,19 @@ export declare function planOversizedRetry(events: RrwebEvent[]): {
23
23
  left: RrwebEvent[];
24
24
  right: RrwebEvent[];
25
25
  };
26
+ /** Leading Meta+FullSnapshot (2) or FullSnapshot alone (1); else 0. */
27
+ export declare function paintBaseEventCount(events: RrwebEvent[]): number;
26
28
  /**
27
29
  * Binary-search the largest prefix that gzips under the pack target.
28
30
  * Remainder is returned for re-queue; null means every event was dropped.
31
+ *
32
+ * When `atomicMinCount` is set (segment-0 paint base), never return a prefix
33
+ * shorter than that count unless the atomic unit itself exceeds `hardCap`
34
+ * (then null — caller should abort as oversized, not ship Meta-only).
29
35
  */
30
- export declare function fitCompressedPrefix(events: RrwebEvent[], compress: CompressEvents, targetBytes?: number, hardCap?: number): Promise<{
36
+ export declare function fitCompressedPrefix(events: RrwebEvent[], compress: CompressEvents, targetBytes?: number, hardCap?: number, opts?: {
37
+ atomicMinCount?: number;
38
+ }): Promise<{
31
39
  events: RrwebEvent[];
32
40
  gzip: Uint8Array;
33
41
  remainder: RrwebEvent[];
@@ -1 +1 @@
1
- {"version":3,"file":"fit_segment.d.ts","sourceRoot":"","sources":["../../src/replay/fit_segment.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,UAAU,EAChB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;AAE3E;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,iBAAiB,EAAE,MAAM,EACzB,OAAO,EAAE,MAAM,EACf,OAAO,SAA4B,GAClC,MAAM,CAER;AAED,2EAA2E;AAC3E,wBAAgB,0BAA0B,CAAC,IAAI,EAAE;IAC/C,YAAY,EAAE,MAAM,CAAC;IACrB,uBAAuB,EAAE,MAAM,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAOV;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,UAAU,EAAE,GACnB;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,UAAU,EAAE,CAAC;IAAC,KAAK,EAAE,UAAU,EAAE,CAAA;CAAE,CAUpF;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,UAAU,EAAE,EACpB,QAAQ,EAAE,cAAc,EACxB,WAAW,SAAkC,EAC7C,OAAO,SAA+B,GACrC,OAAO,CAAC;IACT,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,EAAE,UAAU,EAAE,CAAC;CACzB,GAAG,IAAI,CAAC,CAkCR"}
1
+ {"version":3,"file":"fit_segment.d.ts","sourceRoot":"","sources":["../../src/replay/fit_segment.ts"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,UAAU,EAChB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;AAE3E;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,iBAAiB,EAAE,MAAM,EACzB,OAAO,EAAE,MAAM,EACf,OAAO,SAA4B,GAClC,MAAM,CAER;AAED,2EAA2E;AAC3E,wBAAgB,0BAA0B,CAAC,IAAI,EAAE;IAC/C,YAAY,EAAE,MAAM,CAAC;IACrB,uBAAuB,EAAE,MAAM,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAOV;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,UAAU,EAAE,GACnB;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,UAAU,EAAE,CAAC;IAAC,KAAK,EAAE,UAAU,EAAE,CAAA;CAAE,CAUpF;AAED,uEAAuE;AACvE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,CAYhE;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,UAAU,EAAE,EACpB,QAAQ,EAAE,cAAc,EACxB,WAAW,SAAkC,EAC7C,OAAO,SAA+B,EACtC,IAAI,CAAC,EAAE;IAAE,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,GACjC,OAAO,CAAC;IACT,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,EAAE,UAAU,EAAE,CAAC;CACzB,GAAG,IAAI,CAAC,CA2ER"}
@@ -8,7 +8,8 @@ export type PaintBaseSizeDetails = {
8
8
  fullSnapshotEstimatedBytes?: number;
9
9
  /** Actual gzip size of FullSnapshot alone, when measured. */
10
10
  fullSnapshotCompressedBytes?: number;
11
- maxUncompressedSegmentBytes: number;
11
+ /** Soft estimated take window (not the server uncompressed hard cap). */
12
+ softTakeEstimatedBytes: number;
12
13
  targetCompressedSegmentBytes: number;
13
14
  maxCompressedSegmentBytes: number;
14
15
  } & Record<string, unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"paint_base_size.d.ts","sourceRoot":"","sources":["../../src/replay/paint_base_size.ts"],"names":[],"mappings":"AACA,OAAO,EAML,KAAK,UAAU,EAChB,MAAM,qBAAqB,CAAC;AAE7B,gEAAgE;AAChE,MAAM,MAAM,oBAAoB,GAAG;IACjC,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,0DAA0D;IAC1D,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,4DAA4D;IAC5D,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,6DAA6D;IAC7D,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,2BAA2B,EAAE,MAAM,CAAC;IACpC,4BAA4B,EAAE,MAAM,CAAC;IACrC,yBAAyB,EAAE,MAAM,CAAC;CACnC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE5B,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,UAAU,EAAE,EACpB,IAAI,CAAC,EAAE;IAAE,2BAA2B,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9C,oBAAoB,CAsBtB"}
1
+ {"version":3,"file":"paint_base_size.d.ts","sourceRoot":"","sources":["../../src/replay/paint_base_size.ts"],"names":[],"mappings":"AACA,OAAO,EAML,KAAK,UAAU,EAChB,MAAM,qBAAqB,CAAC;AAE7B,gEAAgE;AAChE,MAAM,MAAM,oBAAoB,GAAG;IACjC,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,0DAA0D;IAC1D,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,4DAA4D;IAC5D,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,6DAA6D;IAC7D,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,yEAAyE;IACzE,sBAAsB,EAAE,MAAM,CAAC;IAC/B,4BAA4B,EAAE,MAAM,CAAC;IACrC,yBAAyB,EAAE,MAAM,CAAC;CACnC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE5B,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,UAAU,EAAE,EACpB,IAAI,CAAC,EAAE;IAAE,2BAA2B,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9C,oBAAoB,CAsBtB"}
@@ -5,8 +5,9 @@ export interface RrwebEvent {
5
5
  timestamp: number;
6
6
  }
7
7
  /**
8
- * Soft uncompressed take size before gzip fitting.
9
- * Keep in sync with ReplayLimits.maxUncompressedSegmentBytes intent.
8
+ * Soft estimated-JSON take size before gzip fitting.
9
+ * Not a hard server limit — packing uses MAX_COMPRESSED_SEGMENT_BYTES.
10
+ * Paint base (Meta+FullSnapshot) is taken atomically and may exceed this.
10
11
  */
11
12
  export declare const SEGMENT_SIZE_BYTES = 800000;
12
13
  /**
@@ -66,12 +67,23 @@ export declare class SegmentBuffer {
66
67
  */
67
68
  trimRing(now?: number, maxBytes?: number): void;
68
69
  shouldFlushBySize(): boolean;
70
+ /**
71
+ * How many leading events form the rrweb paint base (Meta+FullSnapshot or
72
+ * FullSnapshot alone). Returns 0 if the buffer does not start with a paint base.
73
+ */
74
+ paintBaseLength(): number;
69
75
  /**
70
76
  * Take a prefix of events whose estimated JSON size is ~maxBytes.
71
77
  * Always returns at least one event when the buffer is non-empty (caller
72
78
  * must handle a single oversized event).
79
+ *
80
+ * When `keepPaintBaseTogether` is true (segment 0), Meta immediately followed
81
+ * by FullSnapshot is taken as one unit even if estimated size exceeds
82
+ * maxBytes — gzip fitting / hard caps decide whether it can ship.
73
83
  */
74
- takeByEstimatedBytes(maxBytes: number): RrwebEvent[];
84
+ takeByEstimatedBytes(maxBytes: number, opts?: {
85
+ keepPaintBaseTogether?: boolean;
86
+ }): RrwebEvent[];
75
87
  takeAll(): RrwebEvent[];
76
88
  /** Re-queue events at the front (failed upload retry / bisect remainder). */
77
89
  prepend(events: RrwebEvent[]): void;
@@ -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,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,4DAA4D;IAC5D,gBAAgB,IAAI,UAAU,GAAG,SAAS;IAI1C;;;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;;;;GAIG;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,4DAA4D;IAC5D,gBAAgB,IAAI,UAAU,GAAG,SAAS;IAI1C;;;OAGG;IACH,kBAAkB,IAAI,OAAO;IAc7B;;;OAGG;IACH,QAAQ,CAAC,GAAG,SAAa,EAAE,QAAQ,SAAwB,GAAG,IAAI;IAiBlE,iBAAiB,IAAI,OAAO;IAI5B;;;OAGG;IACH,eAAe,IAAI,MAAM;IAczB;;;;;;;;OAQG;IACH,oBAAoB,CAClB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE;QAAE,qBAAqB,CAAC,EAAE,OAAO,CAAA;KAAE,GACzC,UAAU,EAAE;IAqCf,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"}
@@ -0,0 +1,4 @@
1
+ /** Bumped with package.json version for event metadata. */
2
+ export declare const SDK_NAME = "@newtalaria/browser";
3
+ export declare const SDK_VERSION = "0.1.12";
4
+ //# sourceMappingURL=sdk_meta.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sdk_meta.d.ts","sourceRoot":"","sources":["../src/sdk_meta.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,eAAO,MAAM,QAAQ,wBAAwB,CAAC;AAC9C,eAAO,MAAM,WAAW,WAAW,CAAC"}
@@ -17,6 +17,21 @@
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/browser_extension_noise.ts
21
+ var EXTENSION_URL = /(?:chrome|moz|safari|safari-web|ms-browser)-extension:\/\//i;
22
+ var WEBKIT_MASKED = /webkit-masked-url:\/\/hidden\//i;
23
+ function isBrowserExtensionNoise(opts) {
24
+ const filename = opts.filename ?? "";
25
+ if (EXTENSION_URL.test(filename) || WEBKIT_MASKED.test(filename)) {
26
+ return true;
27
+ }
28
+ const stack = opts.stack ?? "";
29
+ if (EXTENSION_URL.test(stack) || WEBKIT_MASKED.test(stack)) {
30
+ return true;
31
+ }
32
+ return false;
33
+ }
34
+
20
35
  // src/utils/opaque_script_error.ts
21
36
  function isOpaqueCrossOriginScriptError(event) {
22
37
  const msg = (event.message ?? "").trim();
@@ -25,6 +40,137 @@
25
40
  return true;
26
41
  }
27
42
 
43
+ // src/utils/browser_context.ts
44
+ function parseBrowserContext(ua = typeof navigator !== "undefined" ? navigator.userAgent : "", language = typeof navigator !== "undefined" ? navigator.language : "") {
45
+ const userAgent = ua || "";
46
+ const lang = language || "";
47
+ let name = "unknown";
48
+ let version = "";
49
+ const rules = [
50
+ { name: "Edge", re: /Edg(?:e|A|iOS)?\/([\d.]+)/ },
51
+ { name: "Opera", re: /OPR\/([\d.]+)/ },
52
+ { name: "Samsung Internet", re: /SamsungBrowser\/([\d.]+)/ },
53
+ { name: "Firefox", re: /Firefox\/([\d.]+)/ },
54
+ { name: "Chrome", re: /(?:Chrome|CriOS)\/([\d.]+)/ },
55
+ { name: "Safari", re: /Version\/([\d.]+).*Safari/ }
56
+ ];
57
+ for (const rule2 of rules) {
58
+ const m = userAgent.match(rule2.re);
59
+ if (m) {
60
+ name = rule2.name;
61
+ version = m[1] ?? "";
62
+ break;
63
+ }
64
+ }
65
+ if (name === "unknown" && /iPhone|iPad|iPod/.test(userAgent) && /AppleWebKit/.test(userAgent)) {
66
+ name = "Safari";
67
+ const m = userAgent.match(/OS ([\d_]+)/);
68
+ if (m?.[1]) version = m[1].replace(/_/g, ".");
69
+ }
70
+ let os = "unknown";
71
+ let osVersion = "";
72
+ const win = userAgent.match(/Windows NT ([\d.]+)/);
73
+ if (win) {
74
+ os = "Windows";
75
+ osVersion = win[1] ?? "";
76
+ } else {
77
+ const android = userAgent.match(/Android ([\d.]+)/);
78
+ if (android) {
79
+ os = "Android";
80
+ osVersion = android[1] ?? "";
81
+ } else {
82
+ const ios = userAgent.match(/(?:iPhone|iPad|iPod).*OS ([\d_]+)/);
83
+ if (ios) {
84
+ os = "iOS";
85
+ osVersion = (ios[1] ?? "").replace(/_/g, ".");
86
+ } else {
87
+ const mac = userAgent.match(/Mac OS X ([\d_]+)/);
88
+ if (mac) {
89
+ os = "macOS";
90
+ osVersion = (mac[1] ?? "").replace(/_/g, ".");
91
+ } else if (/CrOS/.test(userAgent)) {
92
+ os = "Chrome OS";
93
+ } else if (/Linux/.test(userAgent)) {
94
+ os = "Linux";
95
+ }
96
+ }
97
+ }
98
+ }
99
+ let device = "desktop";
100
+ if (/iPad|Tablet|Android(?!.*Mobile)/i.test(userAgent)) {
101
+ device = "tablet";
102
+ } else if (/Mobi|iPhone|iPod|Android.*Mobile/i.test(userAgent)) {
103
+ device = "mobile";
104
+ } else if (!userAgent) {
105
+ device = "unknown";
106
+ }
107
+ return {
108
+ name,
109
+ version,
110
+ os,
111
+ osVersion,
112
+ device,
113
+ language: lang,
114
+ userAgent
115
+ };
116
+ }
117
+ function browserContextTags(ctx) {
118
+ return {
119
+ "browser.name": ctx.name,
120
+ "browser.version": ctx.version || "unknown",
121
+ "os.name": ctx.os,
122
+ "os.version": ctx.osVersion || "unknown",
123
+ device: ctx.device
124
+ };
125
+ }
126
+ async function collectBrowserContext() {
127
+ const base = parseBrowserContext();
128
+ const nav = typeof navigator !== "undefined" ? navigator : void 0;
129
+ const uad = nav?.userAgentData;
130
+ if (!uad) return base;
131
+ if (uad.mobile === true) base.device = "mobile";
132
+ if (uad.platform) {
133
+ const p = uad.platform;
134
+ if (/Windows/i.test(p)) base.os = "Windows";
135
+ else if (/macOS|Mac OS/i.test(p)) base.os = "macOS";
136
+ else if (/Android/i.test(p)) base.os = "Android";
137
+ else if (/iOS|iPhone/i.test(p)) base.os = "iOS";
138
+ else if (/Linux/i.test(p)) base.os = "Linux";
139
+ else if (/Chrome OS|Chromium OS/i.test(p)) base.os = "Chrome OS";
140
+ else base.os = p;
141
+ }
142
+ const brands = uad.brands ?? [];
143
+ const interesting = brands.find(
144
+ (b) => !/Not.?A.?Brand/i.test(b.brand) && !/Chromium/i.test(b.brand)
145
+ );
146
+ if (interesting) {
147
+ base.name = interesting.brand.replace(/^Google /, "");
148
+ base.version = interesting.version;
149
+ }
150
+ try {
151
+ if (typeof uad.getHighEntropyValues === "function") {
152
+ const hi = await uad.getHighEntropyValues([
153
+ "platformVersion",
154
+ "fullVersionList"
155
+ ]);
156
+ if (hi.platformVersion) base.osVersion = hi.platformVersion;
157
+ const full = hi.fullVersionList?.find(
158
+ (b) => !/Not.?A.?Brand/i.test(b.brand) && !/Chromium/i.test(b.brand)
159
+ );
160
+ if (full) {
161
+ base.name = full.brand.replace(/^Google /, "");
162
+ base.version = full.version;
163
+ }
164
+ }
165
+ } catch {
166
+ }
167
+ return base;
168
+ }
169
+
170
+ // src/sdk_meta.ts
171
+ var SDK_NAME = "@newtalaria/browser";
172
+ var SDK_VERSION = "0.1.12";
173
+
28
174
  // src/transport/serverpod.ts
29
175
  var ServerpodTransport = class {
30
176
  constructor(options) {
@@ -206,15 +352,44 @@
206
352
  shouldFlushBySize() {
207
353
  return this.pendingBytes >= SEGMENT_SIZE_BYTES;
208
354
  }
355
+ /**
356
+ * How many leading events form the rrweb paint base (Meta+FullSnapshot or
357
+ * FullSnapshot alone). Returns 0 if the buffer does not start with a paint base.
358
+ */
359
+ paintBaseLength() {
360
+ if (this.events.length === 0) return 0;
361
+ if (this.events[0].type === RRWEB_META && this.events[1]?.type === RRWEB_FULL_SNAPSHOT) {
362
+ return 2;
363
+ }
364
+ if (this.events[0].type === RRWEB_FULL_SNAPSHOT) {
365
+ return 1;
366
+ }
367
+ return 0;
368
+ }
209
369
  /**
210
370
  * Take a prefix of events whose estimated JSON size is ~maxBytes.
211
371
  * Always returns at least one event when the buffer is non-empty (caller
212
372
  * must handle a single oversized event).
373
+ *
374
+ * When `keepPaintBaseTogether` is true (segment 0), Meta immediately followed
375
+ * by FullSnapshot is taken as one unit even if estimated size exceeds
376
+ * maxBytes — gzip fitting / hard caps decide whether it can ship.
213
377
  */
214
- takeByEstimatedBytes(maxBytes) {
378
+ takeByEstimatedBytes(maxBytes, opts) {
215
379
  if (this.events.length === 0) return [];
216
380
  const batch = [];
217
381
  let batchBytes = 0;
382
+ if (opts?.keepPaintBaseTogether) {
383
+ const paintLen = this.paintBaseLength();
384
+ if (paintLen > 0) {
385
+ for (let i = 0; i < paintLen; i++) {
386
+ const ev = this.events.shift();
387
+ batch.push(ev);
388
+ batchBytes += estimateJsonBytes(ev);
389
+ }
390
+ this.recomputeBytes();
391
+ }
392
+ }
218
393
  while (this.events.length > 0) {
219
394
  const next = this.events[0];
220
395
  const nextBytes = estimateJsonBytes(next);
@@ -339,8 +514,53 @@
339
514
  right: events.slice(mid)
340
515
  };
341
516
  }
342
- async function fitCompressedPrefix(events, compress, targetBytes = TARGET_COMPRESSED_SEGMENT_BYTES, hardCap = MAX_COMPRESSED_SEGMENT_BYTES) {
517
+ function paintBaseEventCount(events) {
518
+ if (events.length === 0) return 0;
519
+ if (events[0].type === RRWEB_META && events[1]?.type === RRWEB_FULL_SNAPSHOT) {
520
+ return 2;
521
+ }
522
+ if (events[0].type === RRWEB_FULL_SNAPSHOT) {
523
+ return 1;
524
+ }
525
+ return 0;
526
+ }
527
+ async function fitCompressedPrefix(events, compress, targetBytes = TARGET_COMPRESSED_SEGMENT_BYTES, hardCap = MAX_COMPRESSED_SEGMENT_BYTES, opts) {
343
528
  if (events.length === 0) return null;
529
+ const atomicMin = opts?.atomicMinCount ?? 0;
530
+ if (atomicMin > 0) {
531
+ if (events.length < atomicMin) return null;
532
+ const paint = events.slice(0, atomicMin);
533
+ const paintGzip = await compress(paint);
534
+ if (paintGzip.length > hardCap) {
535
+ return null;
536
+ }
537
+ let lo = atomicMin;
538
+ let hi = events.length;
539
+ let best = {
540
+ events: paint,
541
+ gzip: paintGzip
542
+ };
543
+ while (lo <= hi) {
544
+ const mid = Math.floor((lo + hi) / 2);
545
+ if (mid < atomicMin) {
546
+ lo = atomicMin;
547
+ continue;
548
+ }
549
+ const prefix = events.slice(0, mid);
550
+ const gzip = await compress(prefix);
551
+ if (gzip.length <= targetBytes && gzip.length <= hardCap) {
552
+ best = { events: prefix, gzip };
553
+ lo = mid + 1;
554
+ } else {
555
+ hi = mid - 1;
556
+ }
557
+ }
558
+ return {
559
+ events: best.events,
560
+ gzip: best.gzip,
561
+ remainder: events.slice(best.events.length)
562
+ };
563
+ }
344
564
  let remaining = events;
345
565
  while (remaining.length > 0) {
346
566
  let lo = 1;
@@ -402,7 +622,7 @@
402
622
  const full = events.find((e) => e.type === RRWEB_FULL_SNAPSHOT);
403
623
  const details = {
404
624
  segmentEventTypes: events.map((e) => e.type),
405
- maxUncompressedSegmentBytes: SEGMENT_SIZE_BYTES,
625
+ softTakeEstimatedBytes: SEGMENT_SIZE_BYTES,
406
626
  targetCompressedSegmentBytes: TARGET_COMPRESSED_SEGMENT_BYTES,
407
627
  maxCompressedSegmentBytes: MAX_COMPRESSED_SEGMENT_BYTES
408
628
  };
@@ -13464,6 +13684,8 @@
13464
13684
  this.linkableReplayId = null;
13465
13685
  /** Last error-clip paint/upload failure (cleared on successful segment 0). */
13466
13686
  this.lastReplayCaptureFailure = null;
13687
+ /** Cached once per init for event tags/extra. */
13688
+ this.browserContext = null;
13467
13689
  }
13468
13690
  init(options) {
13469
13691
  if (this.options) {
@@ -13486,6 +13708,10 @@
13486
13708
  this.errorClipDeadlineMs = null;
13487
13709
  this.linkableReplayId = null;
13488
13710
  this.lastReplayCaptureFailure = null;
13711
+ this.browserContext = parseBrowserContext();
13712
+ void collectBrowserContext().then((ctx) => {
13713
+ if (this.options) this.browserContext = ctx;
13714
+ });
13489
13715
  this.buffer = new SegmentBuffer();
13490
13716
  this.uploadChain = Promise.resolve();
13491
13717
  this.sessionSampled = Math.random() < this.options.replaysSessionSampleRate;
@@ -13555,6 +13781,14 @@
13555
13781
  }
13556
13782
  async captureException(error, context) {
13557
13783
  const err = error instanceof Error ? error : new Error(typeof error === "string" ? error : "Unknown error");
13784
+ const filename = typeof context?.extra?.filename === "string" ? context.extra.filename : void 0;
13785
+ if (isBrowserExtensionNoise({
13786
+ message: err.message,
13787
+ stack: err.stack,
13788
+ filename
13789
+ })) {
13790
+ return;
13791
+ }
13558
13792
  await this.capture({
13559
13793
  message: err.message || err.name || "Error",
13560
13794
  level: "error",
@@ -13648,6 +13882,7 @@
13648
13882
  this.closed = false;
13649
13883
  this.linkableReplayId = null;
13650
13884
  this.lastReplayCaptureFailure = null;
13885
+ this.browserContext = null;
13651
13886
  }
13652
13887
  onRrwebEvent(event) {
13653
13888
  if (this.closed || !this.options) return;
@@ -13731,13 +13966,28 @@
13731
13966
  }
13732
13967
  const tags = applyReplayCaptureTags(
13733
13968
  {
13969
+ ...this.browserContext ? browserContextTags(this.browserContext) : {},
13734
13970
  ...this.options.tags ?? {},
13735
13971
  ...args.context?.tags ?? {}
13736
13972
  },
13737
13973
  errorClipOutcome
13738
13974
  );
13739
13975
  const extra = mergeReplayCaptureExtra(
13740
- args.context?.extra,
13976
+ {
13977
+ ...this.browserContext ? {
13978
+ browser: {
13979
+ name: this.browserContext.name,
13980
+ version: this.browserContext.version,
13981
+ os: this.browserContext.os,
13982
+ osVersion: this.browserContext.osVersion,
13983
+ device: this.browserContext.device,
13984
+ language: this.browserContext.language,
13985
+ userAgent: this.browserContext.userAgent
13986
+ }
13987
+ } : {},
13988
+ sdk: { name: SDK_NAME, version: SDK_VERSION },
13989
+ ...args.context?.extra
13990
+ },
13741
13991
  errorClipOutcome
13742
13992
  );
13743
13993
  try {
@@ -13961,6 +14211,13 @@
13961
14211
  const onError = (event) => {
13962
14212
  if (isOpaqueCrossOriginScriptError(event)) return;
13963
14213
  const error = event.error instanceof Error ? event.error : new Error(event.message || "window.onerror");
14214
+ if (isBrowserExtensionNoise({
14215
+ message: event.message || error.message,
14216
+ stack: error.stack,
14217
+ filename: event.filename
14218
+ })) {
14219
+ return;
14220
+ }
13964
14221
  void this.captureException(error, {
13965
14222
  extra: {
13966
14223
  filename: event.filename,
@@ -14055,20 +14312,9 @@
14055
14312
  }
14056
14313
  const { events, gzip } = fitted;
14057
14314
  if (this.segmentIndex === 0 && !events.some((e) => e.type === RRWEB_FULL_SNAPSHOT)) {
14058
- const leftoverFs = this.buffer.peekFullSnapshot();
14059
- let compressedBytes;
14060
- if (leftoverFs) {
14061
- try {
14062
- compressedBytes = (await compressReplayEvents([leftoverFs])).length;
14063
- } catch {
14064
- }
14065
- }
14066
14315
  await this.abortUnusableClip(
14067
14316
  "no_full_snapshot",
14068
- paintBaseSizeDetails(
14069
- leftoverFs ? [...events, leftoverFs] : events,
14070
- { fullSnapshotCompressedBytes: compressedBytes }
14071
- )
14317
+ paintBaseSizeDetails(events)
14072
14318
  );
14073
14319
  return;
14074
14320
  }
@@ -14113,6 +14359,14 @@
14113
14359
  break;
14114
14360
  }
14115
14361
  if (isOversizedSegmentError(error)) {
14362
+ if (this.segmentIndex === 0 && paintBaseEventCount(events) > 0) {
14363
+ await this.abortUnusableClip("oversized_full_snapshot", {
14364
+ source: "server_reject_paint_base",
14365
+ ...paintBaseSizeDetails(events),
14366
+ rejectedCompressedBytes: gzip.length
14367
+ });
14368
+ return;
14369
+ }
14116
14370
  const plan = planOversizedRetry(events);
14117
14371
  if (plan.action === "drop") {
14118
14372
  const droppedType = events[0]?.type;
@@ -14120,7 +14374,7 @@
14120
14374
  "@newtalaria/browser: dropping rrweb event rejected as oversized",
14121
14375
  { type: droppedType }
14122
14376
  );
14123
- if (this.segmentIndex === 0 && droppedType === RRWEB_FULL_SNAPSHOT) {
14377
+ if (droppedType === RRWEB_FULL_SNAPSHOT) {
14124
14378
  await this.abortUnusableClip(
14125
14379
  "oversized_full_snapshot",
14126
14380
  {
@@ -14158,68 +14412,60 @@
14158
14412
  }
14159
14413
  /**
14160
14414
  * Pull the largest event prefix that gzips under the target compressed size.
14161
- * Drops a single event that can never fit — except FullSnapshot on segment 0
14162
- * (abort the clip instead of uploading a blank orphan stream).
14415
+ *
14416
+ * Segment 0 keeps Meta+FullSnapshot (paint base) atomic through take + fit so
14417
+ * a soft estimated-byte window cannot orphan the FullSnapshot.
14163
14418
  */
14164
14419
  async takeFittedSegment() {
14165
- const chunk = this.buffer.takeByEstimatedBytes(SEGMENT_SIZE_BYTES);
14420
+ const isPaintBaseSegment = this.segmentIndex === 0;
14421
+ const chunk = this.buffer.takeByEstimatedBytes(SEGMENT_SIZE_BYTES, {
14422
+ keepPaintBaseTogether: isPaintBaseSegment
14423
+ });
14166
14424
  if (chunk.length === 0) return null;
14425
+ const atomicMin = isPaintBaseSegment ? paintBaseEventCount(chunk) : 0;
14167
14426
  const fitted = await fitCompressedPrefix(
14168
14427
  chunk,
14169
14428
  compressReplayEvents,
14170
14429
  TARGET_COMPRESSED_SEGMENT_BYTES,
14171
- MAX_COMPRESSED_SEGMENT_BYTES
14430
+ MAX_COMPRESSED_SEGMENT_BYTES,
14431
+ atomicMin > 0 ? { atomicMinCount: atomicMin } : void 0
14172
14432
  );
14173
14433
  if (!fitted) {
14174
- const dropped = chunk[0];
14175
- console.warn(
14176
- "@newtalaria/browser: dropping rrweb event that exceeds max segment size",
14177
- { type: dropped?.type }
14178
- );
14179
- if (this.segmentIndex === 0 && dropped?.type === RRWEB_FULL_SNAPSHOT) {
14434
+ if (isPaintBaseSegment && atomicMin > 0) {
14435
+ const paint = chunk.slice(0, atomicMin);
14180
14436
  let compressedBytes;
14181
- try {
14182
- compressedBytes = (await compressReplayEvents([dropped])).length;
14183
- } catch {
14437
+ const full = paint.find((e) => e.type === RRWEB_FULL_SNAPSHOT);
14438
+ if (full) {
14439
+ try {
14440
+ compressedBytes = (await compressReplayEvents([full])).length;
14441
+ } catch {
14442
+ }
14443
+ }
14444
+ if (chunk.length > atomicMin) {
14445
+ this.buffer.prepend(chunk.slice(atomicMin));
14184
14446
  }
14185
14447
  return {
14186
14448
  events: [],
14187
14449
  gzip: new Uint8Array(),
14188
14450
  abortReason: "oversized_full_snapshot",
14189
14451
  abortDetails: {
14190
- source: "fit_compressed_prefix",
14191
- ...paintBaseSizeDetails(chunk, {
14452
+ source: "paint_base_exceeds_hard_cap",
14453
+ ...paintBaseSizeDetails(paint, {
14192
14454
  fullSnapshotCompressedBytes: compressedBytes
14193
14455
  })
14194
14456
  }
14195
14457
  };
14196
14458
  }
14459
+ const dropped = chunk[0];
14460
+ console.warn(
14461
+ "@newtalaria/browser: dropping rrweb event that exceeds max segment size",
14462
+ { type: dropped?.type }
14463
+ );
14197
14464
  if (chunk.length > 1) {
14198
14465
  this.buffer.prepend(chunk.slice(1));
14199
14466
  }
14200
14467
  return this.takeFittedSegment();
14201
14468
  }
14202
- if (this.segmentIndex === 0 && chunk.some((e) => e.type === RRWEB_FULL_SNAPSHOT) && !fitted.events.some((e) => e.type === RRWEB_FULL_SNAPSHOT)) {
14203
- const full = chunk.find((e) => e.type === RRWEB_FULL_SNAPSHOT);
14204
- let compressedBytes;
14205
- if (full) {
14206
- try {
14207
- compressedBytes = (await compressReplayEvents([full])).length;
14208
- } catch {
14209
- }
14210
- }
14211
- return {
14212
- events: [],
14213
- gzip: new Uint8Array(),
14214
- abortReason: "oversized_full_snapshot",
14215
- abortDetails: {
14216
- source: "fit_dropped_full_snapshot",
14217
- ...paintBaseSizeDetails(chunk, {
14218
- fullSnapshotCompressedBytes: compressedBytes
14219
- })
14220
- }
14221
- };
14222
- }
14223
14469
  if (fitted.remainder.length > 0) {
14224
14470
  this.buffer.prepend(fitted.remainder);
14225
14471
  }