@shipeasy/sdk 3.1.0 → 4.2.0

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.
@@ -38,6 +38,7 @@ __export(client_exports, {
38
38
  readConfigOverride: () => readConfigOverride,
39
39
  readExpOverride: () => readExpOverride,
40
40
  readGateOverride: () => readGateOverride,
41
+ see: () => see,
41
42
  shipeasy: () => shipeasy,
42
43
  version: () => version
43
44
  });
@@ -102,8 +103,251 @@ function send(url) {
102
103
  }
103
104
  }
104
105
 
106
+ // src/see/core.ts
107
+ var SEE_MAX_MESSAGE = 500;
108
+ var SEE_MAX_STACK = 8e3;
109
+ var SEE_MAX_SUBJECT = 200;
110
+ var SEE_MAX_EXTRA_VALUE = 200;
111
+ var SEE_MAX_EXTRA_KEYS = 20;
112
+ var SEE_DEDUP_WINDOW_MS = 3e4;
113
+ var SEE_MAX_PER_SESSION = 25;
114
+ function causesThe(subject) {
115
+ return {
116
+ to(outcome) {
117
+ return {
118
+ __seConsequence: true,
119
+ subject: truncate(String(subject), SEE_MAX_SUBJECT),
120
+ outcome: truncate(String(outcome), SEE_MAX_SUBJECT)
121
+ };
122
+ }
123
+ };
124
+ }
125
+ function violation(name) {
126
+ const make = (msg) => ({
127
+ __seViolation: true,
128
+ violationName: String(name),
129
+ ...msg !== void 0 ? { violationMessage: msg } : {},
130
+ message(m) {
131
+ return make(String(m));
132
+ }
133
+ });
134
+ return make();
135
+ }
136
+ function isViolation(p) {
137
+ return typeof p === "object" && p !== null && p.__seViolation === true;
138
+ }
139
+ var EXPECTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-expected");
140
+ function markExpected(err, because) {
141
+ if (typeof err !== "object" || err === null) return;
142
+ try {
143
+ Object.defineProperty(err, EXPECTED_SYM, {
144
+ value: String(because),
145
+ enumerable: false,
146
+ configurable: true
147
+ });
148
+ } catch {
149
+ }
150
+ }
151
+ function isExpected(err) {
152
+ if (typeof err !== "object" || err === null) return false;
153
+ return err[EXPECTED_SYM] !== void 0;
154
+ }
155
+ var REPORTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-reported");
156
+ var SEE_MAX_CAUSE_DEPTH = 8;
157
+ function readReportStamp(err) {
158
+ if (typeof err !== "object" || err === null) return void 0;
159
+ const v = err[REPORTED_SYM];
160
+ return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
161
+ }
162
+ function findCausedBy(problem) {
163
+ let cur = problem;
164
+ const seen = /* @__PURE__ */ new Set();
165
+ for (let depth = 0; depth < SEE_MAX_CAUSE_DEPTH; depth++) {
166
+ if (typeof cur !== "object" || cur === null || seen.has(cur)) break;
167
+ seen.add(cur);
168
+ const stamp = readReportStamp(cur);
169
+ if (stamp) return stamp;
170
+ cur = cur.cause;
171
+ }
172
+ return void 0;
173
+ }
174
+ function markReported(problem, ev) {
175
+ if (!(problem instanceof Error)) return;
176
+ const stamp = {
177
+ error_type: ev.error_type,
178
+ message: ev.message,
179
+ subject: ev.subject,
180
+ outcome: ev.outcome
181
+ };
182
+ if (ev.stack !== void 0) stamp.stack = ev.stack;
183
+ try {
184
+ Object.defineProperty(problem, REPORTED_SYM, {
185
+ value: Object.freeze(stamp),
186
+ enumerable: false,
187
+ configurable: true,
188
+ writable: true
189
+ });
190
+ } catch {
191
+ }
192
+ }
193
+ function truncate(s, max) {
194
+ return s.length > max ? s.slice(0, max) : s;
195
+ }
196
+ function sanitizeExtras(extras) {
197
+ if (!extras || typeof extras !== "object") return void 0;
198
+ const out = {};
199
+ let n = 0;
200
+ for (const [k, v] of Object.entries(extras)) {
201
+ if (v === null || v === void 0) continue;
202
+ if (n >= SEE_MAX_EXTRA_KEYS) break;
203
+ if (typeof v === "string") out[k] = truncate(v, SEE_MAX_EXTRA_VALUE);
204
+ else if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
205
+ else if (typeof v === "boolean") out[k] = v;
206
+ else continue;
207
+ n += 1;
208
+ }
209
+ return n > 0 ? out : void 0;
210
+ }
211
+ function captureCallsiteStack() {
212
+ const raw = new Error().stack;
213
+ if (!raw) return void 0;
214
+ const lines = raw.split("\n");
215
+ const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
216
+ return kept.length ? kept.join("\n") : void 0;
217
+ }
218
+ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
219
+ let errorType;
220
+ let message;
221
+ let stack;
222
+ let kind;
223
+ if (isViolation(problem)) {
224
+ errorType = problem.violationName;
225
+ message = problem.violationMessage ?? problem.violationName;
226
+ stack = captureCallsiteStack();
227
+ kind = kindOverride ?? "violation";
228
+ } else if (problem instanceof Error) {
229
+ errorType = problem.name || "Error";
230
+ message = problem.message || String(problem);
231
+ stack = problem.stack ?? void 0;
232
+ kind = kindOverride ?? "caught";
233
+ } else {
234
+ errorType = "Error";
235
+ message = typeof problem === "string" ? problem : safeString(problem);
236
+ stack = captureCallsiteStack();
237
+ kind = kindOverride ?? "caught";
238
+ }
239
+ const ev = {
240
+ type: "error",
241
+ kind,
242
+ error_type: truncate(errorType, SEE_MAX_SUBJECT),
243
+ message: truncate(message, SEE_MAX_MESSAGE),
244
+ subject: consequence.subject,
245
+ outcome: consequence.outcome,
246
+ side: ctx.side,
247
+ sdk_version: ctx.sdkVersion,
248
+ ts: Date.now()
249
+ };
250
+ if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
251
+ const causedBy = findCausedBy(problem);
252
+ if (causedBy) ev.caused_by = causedBy;
253
+ const cleanExtras = sanitizeExtras(extras);
254
+ if (cleanExtras) ev.extras = cleanExtras;
255
+ if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
256
+ if (ctx.userId) ev.user_id = ctx.userId;
257
+ if (ctx.anonId) ev.anonymous_id = ctx.anonId;
258
+ if (ctx.env) ev.env = ctx.env;
259
+ markReported(problem, ev);
260
+ return ev;
261
+ }
262
+ function safeString(v) {
263
+ try {
264
+ return typeof v === "object" ? JSON.stringify(v) : String(v);
265
+ } catch {
266
+ return String(v);
267
+ }
268
+ }
269
+ var scheduleMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : (cb) => {
270
+ void Promise.resolve().then(cb);
271
+ };
272
+ function startSeeChain(getProblem, dispatch) {
273
+ let subject;
274
+ let outcome;
275
+ let collected;
276
+ let flushed = false;
277
+ scheduleMicrotask(() => {
278
+ if (flushed) return;
279
+ flushed = true;
280
+ dispatch(
281
+ getProblem(),
282
+ // Bare noun phrase — titles render as "… causes the {subject} …", so a
283
+ // leading article would double up ("causes the the app").
284
+ causesThe(subject ?? "app").to(outcome ?? "hit an error"),
285
+ collected
286
+ );
287
+ });
288
+ const tail = {
289
+ extras(x) {
290
+ if (x && typeof x === "object") collected = { ...collected, ...x };
291
+ return tail;
292
+ }
293
+ };
294
+ const step = {
295
+ to(o) {
296
+ outcome = String(o);
297
+ return tail;
298
+ }
299
+ };
300
+ const start = (s) => {
301
+ subject = String(s);
302
+ return step;
303
+ };
304
+ return { causes_the: start, causesThe: start };
305
+ }
306
+ function startSeeViolationChain(name, dispatch) {
307
+ let msg;
308
+ const base = startSeeChain(
309
+ () => msg !== void 0 ? violation(name).message(msg) : violation(name),
310
+ dispatch
311
+ );
312
+ const chain = {
313
+ ...base,
314
+ message(m) {
315
+ msg = String(m);
316
+ return chain;
317
+ }
318
+ };
319
+ return chain;
320
+ }
321
+ function topStackLine(stack) {
322
+ if (!stack) return "";
323
+ for (const line of stack.split("\n")) {
324
+ if (/^\s*at |@|:\d+:\d+/.test(line)) return line.trim().slice(0, 200);
325
+ }
326
+ return "";
327
+ }
328
+ var SeeLimiter = class {
329
+ constructor(maxPerSession = SEE_MAX_PER_SESSION, dedupWindowMs = SEE_DEDUP_WINDOW_MS) {
330
+ this.maxPerSession = maxPerSession;
331
+ this.dedupWindowMs = dedupWindowMs;
332
+ }
333
+ maxPerSession;
334
+ dedupWindowMs;
335
+ lastSent = /* @__PURE__ */ new Map();
336
+ sent = 0;
337
+ shouldSend(ev) {
338
+ if (this.sent >= this.maxPerSession) return false;
339
+ const key = `${ev.kind}|${ev.error_type}|${ev.message.slice(0, 200)}|${topStackLine(ev.stack)}`;
340
+ const now = Date.now();
341
+ const prev = this.lastSent.get(key);
342
+ if (prev !== void 0 && now - prev < this.dedupWindowMs) return false;
343
+ this.lastSent.set(key, now);
344
+ this.sent += 1;
345
+ return true;
346
+ }
347
+ };
348
+
105
349
  // src/client/index.ts
106
- var version = "1.0.0";
350
+ var version = "4.0.0";
107
351
  var FLUSH_INTERVAL_MS = 5e3;
108
352
  var MAX_BUFFER = 100;
109
353
  var ANON_ID_KEY = "__se_anon_id";
@@ -137,6 +381,13 @@ var EventBuffer = class {
137
381
  this.timer = null;
138
382
  }
139
383
  }
384
+ /** True once this visitor has been exposed to ≥1 experiment (this tab or a
385
+ * prior page in the session — the dedup set persists in sessionStorage).
386
+ * Gates auto-metric emission: vitals from non-participants are never read
387
+ * by the analysis pipeline and would be pure AE write cost (see cost.md). */
388
+ hasExposures() {
389
+ return this.exposureSeen.size > 0;
390
+ }
140
391
  pushExposure(experiment, group, userId, anonId) {
141
392
  const key = `${userId || anonId}:${experiment}`;
142
393
  if (this.exposureSeen.has(key)) return;
@@ -202,16 +453,29 @@ var EventBuffer = class {
202
453
  flush(useBeacon = false) {
203
454
  if (!this.queue.length) return;
204
455
  const batch = this.queue.splice(0);
205
- const body = JSON.stringify({ events: batch });
456
+ this.send(batch, useBeacon);
457
+ }
458
+ /**
459
+ * Bypass the 5s queue and ship events immediately — used by see() error
460
+ * reporting so occurrences land near-real-time and survive page unload.
461
+ * Beacon-first (fire-and-forget, unload-safe), keepalive fetch fallback.
462
+ */
463
+ sendNow(events) {
464
+ this.send(events, true);
465
+ }
466
+ send(batch, useBeacon) {
206
467
  if (useBeacon && typeof navigator !== "undefined" && navigator.sendBeacon) {
207
468
  const beaconBody = JSON.stringify({ k: this.sdkKey, events: batch });
208
- navigator.sendBeacon(this.collectUrl, new Blob([beaconBody], { type: "text/plain" }));
209
- return;
469
+ try {
470
+ if (navigator.sendBeacon(this.collectUrl, new Blob([beaconBody], { type: "text/plain" })))
471
+ return;
472
+ } catch {
473
+ }
210
474
  }
211
475
  fetch(this.collectUrl, {
212
476
  method: "POST",
213
477
  headers: { "X-SDK-Key": this.sdkKey, "Content-Type": "application/json" },
214
- body,
478
+ body: JSON.stringify({ events: batch }),
215
479
  keepalive: true
216
480
  }).catch(() => {
217
481
  });
@@ -228,14 +492,24 @@ var EventBuffer = class {
228
492
  });
229
493
  }
230
494
  };
231
- var MAX_ERRORS_PER_SESSION = 5;
232
- function installAutoGuardrails(buffer, userId, anonId, groups) {
495
+ function endpointTemplate(rawUrl) {
496
+ const isIdSegment = (seg) => /^\d+$/.test(seg) || /^0x[0-9a-f]+$/i.test(seg) || /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(seg) || /^[0-9a-f]{8,}$/i.test(seg) || seg.length >= 12 && /\d/.test(seg) && /[a-z]/i.test(seg);
497
+ let u;
498
+ try {
499
+ u = new URL(rawUrl, typeof location !== "undefined" ? location.href : void 0);
500
+ } catch {
501
+ return (rawUrl.split(/[?#]/)[0] ?? "").slice(0, 120);
502
+ }
503
+ const path = u.pathname.split("/").map((seg) => seg && isIdSegment(seg) ? ":id" : seg).join("/");
504
+ const sameOrigin = typeof location !== "undefined" && u.origin === location.origin;
505
+ return ((sameOrigin ? "" : u.host) + path).slice(0, 120);
506
+ }
507
+ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignoreUrlPrefixes, always = false) {
233
508
  if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") return;
509
+ const shouldEmit = () => always || buffer.hasExposures();
234
510
  let lcp = null;
235
511
  let inp = null;
236
512
  let clsBad = false;
237
- let jsErrorCount = 0;
238
- let netErrorCount = 0;
239
513
  let navTimingFlushed = false;
240
514
  if (groups.vitals) {
241
515
  try {
@@ -274,68 +548,71 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
274
548
  if (groups.errors) {
275
549
  const origOnError = window.onerror;
276
550
  window.onerror = (msg, source, lineno, _colno, err) => {
277
- if (jsErrorCount < MAX_ERRORS_PER_SESSION) {
278
- jsErrorCount += 1;
279
- buffer.pushMetric("__auto_js_error", userId, anonId, {
280
- value: 1,
281
- kind: "exception",
282
- message: typeof msg === "string" ? msg.slice(0, 200) : String(err ?? "").slice(0, 200),
283
- source: typeof source === "string" ? source.slice(0, 200) : "",
284
- line: lineno ?? 0
285
- });
551
+ if (!isExpected(err)) {
552
+ const problem = err ?? (typeof msg === "string" && msg ? msg : "Unknown error");
553
+ reportSee(
554
+ problem,
555
+ causesThe("page").to("hit an unhandled error"),
556
+ {
557
+ source: typeof source === "string" ? source : void 0,
558
+ line: lineno ?? void 0
559
+ },
560
+ "uncaught"
561
+ );
286
562
  }
287
563
  if (typeof origOnError === "function") return origOnError(msg, source, lineno, _colno, err);
288
564
  return false;
289
565
  };
290
566
  window.addEventListener("unhandledrejection", (e) => {
291
- if (jsErrorCount < MAX_ERRORS_PER_SESSION) {
292
- jsErrorCount += 1;
293
- const reason = e.reason;
294
- const message = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : String(reason);
295
- buffer.pushMetric("__auto_js_error", userId, anonId, {
296
- value: 1,
297
- kind: "unhandled_rejection",
298
- message: message.slice(0, 200)
299
- });
300
- }
567
+ const reason = e.reason;
568
+ if (isExpected(reason)) return;
569
+ reportSee(
570
+ reason ?? "Unhandled promise rejection",
571
+ causesThe("page").to("hit an unhandled promise rejection"),
572
+ void 0,
573
+ "unhandled_rejection"
574
+ );
301
575
  });
302
576
  const origFetch = window.fetch;
303
577
  window.fetch = async function(...args) {
304
578
  const startedAt = typeof performance !== "undefined" ? performance.now() : 0;
305
579
  const url = typeof args[0] === "string" ? args[0] : args[0].toString();
580
+ const ignored = ignoreUrlPrefixes.some((p) => p && url.startsWith(p));
581
+ const bareUrl = url.split("?")[0].slice(0, 200);
306
582
  let res;
307
583
  try {
308
584
  res = await origFetch.apply(this, args);
309
585
  } catch (err) {
310
- if (netErrorCount < MAX_ERRORS_PER_SESSION) {
311
- netErrorCount += 1;
312
- buffer.pushMetric("__auto_network_error", userId, anonId, {
313
- value: 1,
314
- kind: "network",
315
- status: 0,
316
- url: url.slice(0, 200)
317
- });
586
+ if (!ignored && !isExpected(err)) {
587
+ reportSee(
588
+ violation("NetworkError").message(`request to ${bareUrl} failed`),
589
+ causesThe(`request to ${endpointTemplate(url)}`).to("get no response"),
590
+ { status: 0, url: url.slice(0, 200) },
591
+ "network"
592
+ );
318
593
  }
319
594
  throw err;
320
595
  }
321
- if (res.status >= 500 && netErrorCount < MAX_ERRORS_PER_SESSION) {
322
- netErrorCount += 1;
596
+ if (!ignored && res.status >= 500) {
323
597
  const elapsed = typeof performance !== "undefined" ? performance.now() - startedAt : 0;
324
- buffer.pushMetric("__auto_network_error", userId, anonId, {
325
- value: 1,
326
- kind: "5xx",
327
- status: res.status,
328
- url: url.slice(0, 200),
329
- duration_ms: Math.round(elapsed)
330
- });
598
+ reportSee(
599
+ violation("Http5xx").message(`request to ${bareUrl} returned ${res.status}`),
600
+ causesThe(`request to ${endpointTemplate(url)}`).to("fail with a server error"),
601
+ { status: res.status, url: url.slice(0, 200), duration_ms: Math.round(elapsed) },
602
+ "network"
603
+ );
331
604
  }
332
605
  return res;
333
606
  };
334
607
  }
335
608
  const flushNavTiming = () => {
336
609
  if (navTimingFlushed) return;
610
+ if (!groups.vitals) {
611
+ navTimingFlushed = true;
612
+ return;
613
+ }
614
+ if (!shouldEmit()) return;
337
615
  navTimingFlushed = true;
338
- if (!groups.vitals) return;
339
616
  try {
340
617
  const navList = performance.getEntriesByType("navigation");
341
618
  const nav = navList[0];
@@ -370,7 +647,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
370
647
  };
371
648
  if (groups.engagement) {
372
649
  try {
373
- buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
650
+ if (shouldEmit()) buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
374
651
  } catch {
375
652
  }
376
653
  let lastEmit = Date.now();
@@ -378,6 +655,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
378
655
  document.addEventListener("visibilitychange", () => {
379
656
  if (document.visibilityState !== "visible") return;
380
657
  if (Date.now() - lastEmit < SESSION_GAP_MS) return;
658
+ if (!shouldEmit()) return;
381
659
  try {
382
660
  buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
383
661
  lastEmit = Date.now();
@@ -400,7 +678,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
400
678
  }
401
679
  const flushOnHide = () => {
402
680
  flushNavTiming();
403
- if (groups.vitals) {
681
+ if (groups.vitals && shouldEmit()) {
404
682
  if (lcp !== null) buffer.pushMetric("__auto_lcp", userId, anonId, { value: lcp });
405
683
  if (inp !== null) buffer.pushMetric("__auto_inp", userId, anonId, { value: inp });
406
684
  if (clsBad) buffer.pushMetric("__auto_cls_binary", userId, anonId, { value: 1 });
@@ -464,6 +742,83 @@ function collectBrowserAttrs() {
464
742
  }
465
743
  return attrs;
466
744
  }
745
+ function collectSeeEnv() {
746
+ const out = {};
747
+ if (typeof navigator === "undefined") return out;
748
+ const nav = navigator;
749
+ const ua = typeof nav.userAgent === "string" ? nav.userAgent : "";
750
+ const browser = parseUaBrowser(ua);
751
+ if (browser) out["env.browser"] = browser;
752
+ const os = parseUaOs(ua) ?? nav.userAgentData?.platform;
753
+ if (os) out["env.os"] = os;
754
+ out["env.device"] = typeof nav.userAgentData?.mobile === "boolean" ? nav.userAgentData.mobile ? "mobile" : "desktop" : /iPad|Tablet/.test(ua) ? "tablet" : /Mobi|iPhone|Android.*Mobile/.test(ua) ? "mobile" : "desktop";
755
+ try {
756
+ if (nav.language) out["env.lang"] = nav.language;
757
+ } catch {
758
+ }
759
+ try {
760
+ if (typeof nav.onLine === "boolean") out["env.online"] = nav.onLine;
761
+ } catch {
762
+ }
763
+ try {
764
+ if (typeof nav.hardwareConcurrency === "number") out["env.cores"] = nav.hardwareConcurrency;
765
+ } catch {
766
+ }
767
+ try {
768
+ if (typeof nav.deviceMemory === "number") out["env.memory_gb"] = nav.deviceMemory;
769
+ } catch {
770
+ }
771
+ try {
772
+ const et = nav.connection?.effectiveType;
773
+ if (et) out["env.connection"] = et;
774
+ } catch {
775
+ }
776
+ try {
777
+ if (typeof window !== "undefined" && window.innerWidth && window.innerHeight) {
778
+ out["env.viewport"] = `${window.innerWidth}\xD7${window.innerHeight}`;
779
+ }
780
+ if (typeof window !== "undefined" && typeof window.devicePixelRatio === "number") {
781
+ out["env.dpr"] = window.devicePixelRatio;
782
+ }
783
+ if (typeof screen !== "undefined" && screen.width && screen.height) {
784
+ out["env.screen"] = `${screen.width}\xD7${screen.height}`;
785
+ }
786
+ } catch {
787
+ }
788
+ try {
789
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
790
+ if (tz) out["env.tz"] = tz;
791
+ } catch {
792
+ }
793
+ return out;
794
+ }
795
+ function parseUaBrowser(ua) {
796
+ const tests = [
797
+ [/Edg(?:A|iOS)?\/(\d+)/, "Edge"],
798
+ [/(?:OPR|Opera)\/(\d+)/, "Opera"],
799
+ [/(?:Firefox|FxiOS)\/(\d+)/, "Firefox"],
800
+ [/(?:Chrome|CriOS)\/(\d+)/, "Chrome"],
801
+ [/Version\/(\d+)[.\d]* (?:Mobile.*)?Safari/, "Safari"]
802
+ ];
803
+ for (const [re, name] of tests) {
804
+ const m = re.exec(ua);
805
+ if (m) return `${name} ${m[1]}`;
806
+ }
807
+ return void 0;
808
+ }
809
+ function parseUaOs(ua) {
810
+ if (/Windows NT 10/.test(ua)) return "Windows 10/11";
811
+ if (/Windows NT/.test(ua)) return "Windows";
812
+ let m = /Mac OS X (\d+)[._](\d+)/.exec(ua);
813
+ if (m) return `macOS ${m[1]}.${m[2]}`;
814
+ if (/Macintosh/.test(ua)) return "macOS";
815
+ m = /Android (\d+)/.exec(ua);
816
+ if (m) return `Android ${m[1]}`;
817
+ m = /(?:iPhone|iPad)[^)]* OS (\d+)/.exec(ua);
818
+ if (m) return `iOS ${m[1]}`;
819
+ if (/Linux/.test(ua)) return "Linux";
820
+ return void 0;
821
+ }
467
822
  function readExperimentOverridesFromUrl() {
468
823
  if (typeof window === "undefined") return {};
469
824
  const out = {};
@@ -483,12 +838,14 @@ var FlagsClientBrowser = class {
483
838
  baseUrl;
484
839
  autoGuardrails;
485
840
  autoGuardrailGroups;
841
+ autoCollectAlways;
486
842
  env;
487
843
  evalResult = null;
488
844
  anonId;
489
845
  userId = "";
490
846
  buffer;
491
847
  telemetry;
848
+ seeLimiter = new SeeLimiter();
492
849
  guardrailsInstalled = false;
493
850
  listeners = /* @__PURE__ */ new Set();
494
851
  overrideListenerInstalled = false;
@@ -504,6 +861,7 @@ var FlagsClientBrowser = class {
504
861
  this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
505
862
  this.env = opts.env ?? "prod";
506
863
  this.autoGuardrails = opts.autoGuardrails !== false;
864
+ this.autoCollectAlways = opts.autoCollectAlways === true;
507
865
  const g = opts.autoGuardrailGroups ?? {};
508
866
  this.autoGuardrailGroups = {
509
867
  vitals: g.vitals ?? this.autoGuardrails,
@@ -548,10 +906,39 @@ var FlagsClientBrowser = class {
548
906
  const anyGroupOn = this.autoGuardrailGroups.vitals || this.autoGuardrailGroups.errors || this.autoGuardrailGroups.engagement;
549
907
  if (anyGroupOn && !this.guardrailsInstalled) {
550
908
  this.guardrailsInstalled = true;
551
- installAutoGuardrails(this.buffer, this.userId, this.anonId, this.autoGuardrailGroups);
909
+ installAutoGuardrails(
910
+ this.buffer,
911
+ this.userId,
912
+ this.anonId,
913
+ this.autoGuardrailGroups,
914
+ (problem, consequence, extras, kind) => this.reportError(problem, consequence, extras, kind),
915
+ [`${this.baseUrl}/`, DEFAULT_TELEMETRY_URL],
916
+ this.autoCollectAlways
917
+ );
552
918
  }
553
919
  this.notify();
554
920
  }
921
+ /**
922
+ * Report a structured error into the errors primitive. Flushes immediately
923
+ * (beacon-first) — error occurrences are near-real-time, never queued behind
924
+ * the 5s metric batch. Spam-guarded by a 30s dedup window + per-session cap.
925
+ */
926
+ reportError(problem, consequence, extras, kind) {
927
+ try {
928
+ const enriched = { ...collectSeeEnv(), ...extras };
929
+ const ev = buildSeeEvent(problem, consequence, enriched, {
930
+ side: "client",
931
+ sdkVersion: version,
932
+ env: this.env,
933
+ url: typeof window !== "undefined" && window.location ? window.location.href : void 0,
934
+ userId: this.userId || void 0,
935
+ anonId: this.anonId
936
+ }, kind);
937
+ if (!this.seeLimiter.shouldSend(ev)) return;
938
+ this.buffer.sendNow([ev]);
939
+ } catch {
940
+ }
941
+ }
555
942
  get ready() {
556
943
  return this.evalResult !== null;
557
944
  }
@@ -783,13 +1170,15 @@ var _client = null;
783
1170
  function shipeasy(opts) {
784
1171
  const ac = opts.autoCollect;
785
1172
  const blanket = ac === false ? false : true;
786
- const groups = ac && typeof ac === "object" ? ac : void 0;
1173
+ const acObj = ac && typeof ac === "object" ? ac : void 0;
1174
+ const groups = acObj ? { vitals: acObj.vitals, errors: acObj.errors, engagement: acObj.engagement } : void 0;
787
1175
  const baseUrl = opts.baseUrl ?? "https://cdn.shipeasy.ai";
788
1176
  const client = configureShipeasy({
789
1177
  sdkKey: opts.clientKey,
790
1178
  baseUrl,
791
1179
  autoGuardrails: blanket,
792
1180
  autoGuardrailGroups: groups,
1181
+ autoCollectAlways: acObj?.always === true,
793
1182
  disableTelemetry: opts.disableTelemetry
794
1183
  });
795
1184
  injectI18nLoader(opts.clientKey, baseUrl, opts.i18nProfile);
@@ -951,6 +1340,20 @@ var flags = {
951
1340
  return _client?.ready ?? false;
952
1341
  }
953
1342
  };
1343
+ function dispatchSee(problem, consequence, extras, kind) {
1344
+ if (!_client) {
1345
+ console.warn("[shipeasy] see() called before shipeasy({ clientKey }) \u2014 error dropped");
1346
+ return;
1347
+ }
1348
+ _client.reportError(problem, consequence, extras, kind);
1349
+ }
1350
+ var see = Object.assign(
1351
+ (problem) => startSeeChain(() => problem, dispatchSee),
1352
+ {
1353
+ Violation: (name) => startSeeViolationChain(name, dispatchSee),
1354
+ ControlFlowException: markExpected
1355
+ }
1356
+ );
954
1357
  var LABEL_MARKER_START = "\uFFF9";
955
1358
  var LABEL_MARKER_SEP = "\uFFFA";
956
1359
  var LABEL_MARKER_END = "\uFFFB";
@@ -1203,6 +1606,7 @@ var i18n = {
1203
1606
  readConfigOverride,
1204
1607
  readExpOverride,
1205
1608
  readGateOverride,
1609
+ see,
1206
1610
  shipeasy,
1207
1611
  version
1208
1612
  });