@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.
@@ -57,8 +57,251 @@ function send(url) {
57
57
  }
58
58
  }
59
59
 
60
+ // src/see/core.ts
61
+ var SEE_MAX_MESSAGE = 500;
62
+ var SEE_MAX_STACK = 8e3;
63
+ var SEE_MAX_SUBJECT = 200;
64
+ var SEE_MAX_EXTRA_VALUE = 200;
65
+ var SEE_MAX_EXTRA_KEYS = 20;
66
+ var SEE_DEDUP_WINDOW_MS = 3e4;
67
+ var SEE_MAX_PER_SESSION = 25;
68
+ function causesThe(subject) {
69
+ return {
70
+ to(outcome) {
71
+ return {
72
+ __seConsequence: true,
73
+ subject: truncate(String(subject), SEE_MAX_SUBJECT),
74
+ outcome: truncate(String(outcome), SEE_MAX_SUBJECT)
75
+ };
76
+ }
77
+ };
78
+ }
79
+ function violation(name) {
80
+ const make = (msg) => ({
81
+ __seViolation: true,
82
+ violationName: String(name),
83
+ ...msg !== void 0 ? { violationMessage: msg } : {},
84
+ message(m) {
85
+ return make(String(m));
86
+ }
87
+ });
88
+ return make();
89
+ }
90
+ function isViolation(p) {
91
+ return typeof p === "object" && p !== null && p.__seViolation === true;
92
+ }
93
+ var EXPECTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-expected");
94
+ function markExpected(err, because) {
95
+ if (typeof err !== "object" || err === null) return;
96
+ try {
97
+ Object.defineProperty(err, EXPECTED_SYM, {
98
+ value: String(because),
99
+ enumerable: false,
100
+ configurable: true
101
+ });
102
+ } catch {
103
+ }
104
+ }
105
+ function isExpected(err) {
106
+ if (typeof err !== "object" || err === null) return false;
107
+ return err[EXPECTED_SYM] !== void 0;
108
+ }
109
+ var REPORTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-reported");
110
+ var SEE_MAX_CAUSE_DEPTH = 8;
111
+ function readReportStamp(err) {
112
+ if (typeof err !== "object" || err === null) return void 0;
113
+ const v = err[REPORTED_SYM];
114
+ return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
115
+ }
116
+ function findCausedBy(problem) {
117
+ let cur = problem;
118
+ const seen = /* @__PURE__ */ new Set();
119
+ for (let depth = 0; depth < SEE_MAX_CAUSE_DEPTH; depth++) {
120
+ if (typeof cur !== "object" || cur === null || seen.has(cur)) break;
121
+ seen.add(cur);
122
+ const stamp = readReportStamp(cur);
123
+ if (stamp) return stamp;
124
+ cur = cur.cause;
125
+ }
126
+ return void 0;
127
+ }
128
+ function markReported(problem, ev) {
129
+ if (!(problem instanceof Error)) return;
130
+ const stamp = {
131
+ error_type: ev.error_type,
132
+ message: ev.message,
133
+ subject: ev.subject,
134
+ outcome: ev.outcome
135
+ };
136
+ if (ev.stack !== void 0) stamp.stack = ev.stack;
137
+ try {
138
+ Object.defineProperty(problem, REPORTED_SYM, {
139
+ value: Object.freeze(stamp),
140
+ enumerable: false,
141
+ configurable: true,
142
+ writable: true
143
+ });
144
+ } catch {
145
+ }
146
+ }
147
+ function truncate(s, max) {
148
+ return s.length > max ? s.slice(0, max) : s;
149
+ }
150
+ function sanitizeExtras(extras) {
151
+ if (!extras || typeof extras !== "object") return void 0;
152
+ const out = {};
153
+ let n = 0;
154
+ for (const [k, v] of Object.entries(extras)) {
155
+ if (v === null || v === void 0) continue;
156
+ if (n >= SEE_MAX_EXTRA_KEYS) break;
157
+ if (typeof v === "string") out[k] = truncate(v, SEE_MAX_EXTRA_VALUE);
158
+ else if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
159
+ else if (typeof v === "boolean") out[k] = v;
160
+ else continue;
161
+ n += 1;
162
+ }
163
+ return n > 0 ? out : void 0;
164
+ }
165
+ function captureCallsiteStack() {
166
+ const raw = new Error().stack;
167
+ if (!raw) return void 0;
168
+ const lines = raw.split("\n");
169
+ const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
170
+ return kept.length ? kept.join("\n") : void 0;
171
+ }
172
+ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
173
+ let errorType;
174
+ let message;
175
+ let stack;
176
+ let kind;
177
+ if (isViolation(problem)) {
178
+ errorType = problem.violationName;
179
+ message = problem.violationMessage ?? problem.violationName;
180
+ stack = captureCallsiteStack();
181
+ kind = kindOverride ?? "violation";
182
+ } else if (problem instanceof Error) {
183
+ errorType = problem.name || "Error";
184
+ message = problem.message || String(problem);
185
+ stack = problem.stack ?? void 0;
186
+ kind = kindOverride ?? "caught";
187
+ } else {
188
+ errorType = "Error";
189
+ message = typeof problem === "string" ? problem : safeString(problem);
190
+ stack = captureCallsiteStack();
191
+ kind = kindOverride ?? "caught";
192
+ }
193
+ const ev = {
194
+ type: "error",
195
+ kind,
196
+ error_type: truncate(errorType, SEE_MAX_SUBJECT),
197
+ message: truncate(message, SEE_MAX_MESSAGE),
198
+ subject: consequence.subject,
199
+ outcome: consequence.outcome,
200
+ side: ctx.side,
201
+ sdk_version: ctx.sdkVersion,
202
+ ts: Date.now()
203
+ };
204
+ if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
205
+ const causedBy = findCausedBy(problem);
206
+ if (causedBy) ev.caused_by = causedBy;
207
+ const cleanExtras = sanitizeExtras(extras);
208
+ if (cleanExtras) ev.extras = cleanExtras;
209
+ if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
210
+ if (ctx.userId) ev.user_id = ctx.userId;
211
+ if (ctx.anonId) ev.anonymous_id = ctx.anonId;
212
+ if (ctx.env) ev.env = ctx.env;
213
+ markReported(problem, ev);
214
+ return ev;
215
+ }
216
+ function safeString(v) {
217
+ try {
218
+ return typeof v === "object" ? JSON.stringify(v) : String(v);
219
+ } catch {
220
+ return String(v);
221
+ }
222
+ }
223
+ var scheduleMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : (cb) => {
224
+ void Promise.resolve().then(cb);
225
+ };
226
+ function startSeeChain(getProblem, dispatch) {
227
+ let subject;
228
+ let outcome;
229
+ let collected;
230
+ let flushed = false;
231
+ scheduleMicrotask(() => {
232
+ if (flushed) return;
233
+ flushed = true;
234
+ dispatch(
235
+ getProblem(),
236
+ // Bare noun phrase — titles render as "… causes the {subject} …", so a
237
+ // leading article would double up ("causes the the app").
238
+ causesThe(subject ?? "app").to(outcome ?? "hit an error"),
239
+ collected
240
+ );
241
+ });
242
+ const tail = {
243
+ extras(x) {
244
+ if (x && typeof x === "object") collected = { ...collected, ...x };
245
+ return tail;
246
+ }
247
+ };
248
+ const step = {
249
+ to(o) {
250
+ outcome = String(o);
251
+ return tail;
252
+ }
253
+ };
254
+ const start = (s) => {
255
+ subject = String(s);
256
+ return step;
257
+ };
258
+ return { causes_the: start, causesThe: start };
259
+ }
260
+ function startSeeViolationChain(name, dispatch) {
261
+ let msg;
262
+ const base = startSeeChain(
263
+ () => msg !== void 0 ? violation(name).message(msg) : violation(name),
264
+ dispatch
265
+ );
266
+ const chain = {
267
+ ...base,
268
+ message(m) {
269
+ msg = String(m);
270
+ return chain;
271
+ }
272
+ };
273
+ return chain;
274
+ }
275
+ function topStackLine(stack) {
276
+ if (!stack) return "";
277
+ for (const line of stack.split("\n")) {
278
+ if (/^\s*at |@|:\d+:\d+/.test(line)) return line.trim().slice(0, 200);
279
+ }
280
+ return "";
281
+ }
282
+ var SeeLimiter = class {
283
+ constructor(maxPerSession = SEE_MAX_PER_SESSION, dedupWindowMs = SEE_DEDUP_WINDOW_MS) {
284
+ this.maxPerSession = maxPerSession;
285
+ this.dedupWindowMs = dedupWindowMs;
286
+ }
287
+ maxPerSession;
288
+ dedupWindowMs;
289
+ lastSent = /* @__PURE__ */ new Map();
290
+ sent = 0;
291
+ shouldSend(ev) {
292
+ if (this.sent >= this.maxPerSession) return false;
293
+ const key = `${ev.kind}|${ev.error_type}|${ev.message.slice(0, 200)}|${topStackLine(ev.stack)}`;
294
+ const now = Date.now();
295
+ const prev = this.lastSent.get(key);
296
+ if (prev !== void 0 && now - prev < this.dedupWindowMs) return false;
297
+ this.lastSent.set(key, now);
298
+ this.sent += 1;
299
+ return true;
300
+ }
301
+ };
302
+
60
303
  // src/client/index.ts
61
- var version = "1.0.0";
304
+ var version = "4.0.0";
62
305
  var FLUSH_INTERVAL_MS = 5e3;
63
306
  var MAX_BUFFER = 100;
64
307
  var ANON_ID_KEY = "__se_anon_id";
@@ -92,6 +335,13 @@ var EventBuffer = class {
92
335
  this.timer = null;
93
336
  }
94
337
  }
338
+ /** True once this visitor has been exposed to ≥1 experiment (this tab or a
339
+ * prior page in the session — the dedup set persists in sessionStorage).
340
+ * Gates auto-metric emission: vitals from non-participants are never read
341
+ * by the analysis pipeline and would be pure AE write cost (see cost.md). */
342
+ hasExposures() {
343
+ return this.exposureSeen.size > 0;
344
+ }
95
345
  pushExposure(experiment, group, userId, anonId) {
96
346
  const key = `${userId || anonId}:${experiment}`;
97
347
  if (this.exposureSeen.has(key)) return;
@@ -157,16 +407,29 @@ var EventBuffer = class {
157
407
  flush(useBeacon = false) {
158
408
  if (!this.queue.length) return;
159
409
  const batch = this.queue.splice(0);
160
- const body = JSON.stringify({ events: batch });
410
+ this.send(batch, useBeacon);
411
+ }
412
+ /**
413
+ * Bypass the 5s queue and ship events immediately — used by see() error
414
+ * reporting so occurrences land near-real-time and survive page unload.
415
+ * Beacon-first (fire-and-forget, unload-safe), keepalive fetch fallback.
416
+ */
417
+ sendNow(events) {
418
+ this.send(events, true);
419
+ }
420
+ send(batch, useBeacon) {
161
421
  if (useBeacon && typeof navigator !== "undefined" && navigator.sendBeacon) {
162
422
  const beaconBody = JSON.stringify({ k: this.sdkKey, events: batch });
163
- navigator.sendBeacon(this.collectUrl, new Blob([beaconBody], { type: "text/plain" }));
164
- return;
423
+ try {
424
+ if (navigator.sendBeacon(this.collectUrl, new Blob([beaconBody], { type: "text/plain" })))
425
+ return;
426
+ } catch {
427
+ }
165
428
  }
166
429
  fetch(this.collectUrl, {
167
430
  method: "POST",
168
431
  headers: { "X-SDK-Key": this.sdkKey, "Content-Type": "application/json" },
169
- body,
432
+ body: JSON.stringify({ events: batch }),
170
433
  keepalive: true
171
434
  }).catch(() => {
172
435
  });
@@ -183,14 +446,24 @@ var EventBuffer = class {
183
446
  });
184
447
  }
185
448
  };
186
- var MAX_ERRORS_PER_SESSION = 5;
187
- function installAutoGuardrails(buffer, userId, anonId, groups) {
449
+ function endpointTemplate(rawUrl) {
450
+ 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);
451
+ let u;
452
+ try {
453
+ u = new URL(rawUrl, typeof location !== "undefined" ? location.href : void 0);
454
+ } catch {
455
+ return (rawUrl.split(/[?#]/)[0] ?? "").slice(0, 120);
456
+ }
457
+ const path = u.pathname.split("/").map((seg) => seg && isIdSegment(seg) ? ":id" : seg).join("/");
458
+ const sameOrigin = typeof location !== "undefined" && u.origin === location.origin;
459
+ return ((sameOrigin ? "" : u.host) + path).slice(0, 120);
460
+ }
461
+ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignoreUrlPrefixes, always = false) {
188
462
  if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") return;
463
+ const shouldEmit = () => always || buffer.hasExposures();
189
464
  let lcp = null;
190
465
  let inp = null;
191
466
  let clsBad = false;
192
- let jsErrorCount = 0;
193
- let netErrorCount = 0;
194
467
  let navTimingFlushed = false;
195
468
  if (groups.vitals) {
196
469
  try {
@@ -229,68 +502,71 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
229
502
  if (groups.errors) {
230
503
  const origOnError = window.onerror;
231
504
  window.onerror = (msg, source, lineno, _colno, err) => {
232
- if (jsErrorCount < MAX_ERRORS_PER_SESSION) {
233
- jsErrorCount += 1;
234
- buffer.pushMetric("__auto_js_error", userId, anonId, {
235
- value: 1,
236
- kind: "exception",
237
- message: typeof msg === "string" ? msg.slice(0, 200) : String(err ?? "").slice(0, 200),
238
- source: typeof source === "string" ? source.slice(0, 200) : "",
239
- line: lineno ?? 0
240
- });
505
+ if (!isExpected(err)) {
506
+ const problem = err ?? (typeof msg === "string" && msg ? msg : "Unknown error");
507
+ reportSee(
508
+ problem,
509
+ causesThe("page").to("hit an unhandled error"),
510
+ {
511
+ source: typeof source === "string" ? source : void 0,
512
+ line: lineno ?? void 0
513
+ },
514
+ "uncaught"
515
+ );
241
516
  }
242
517
  if (typeof origOnError === "function") return origOnError(msg, source, lineno, _colno, err);
243
518
  return false;
244
519
  };
245
520
  window.addEventListener("unhandledrejection", (e) => {
246
- if (jsErrorCount < MAX_ERRORS_PER_SESSION) {
247
- jsErrorCount += 1;
248
- const reason = e.reason;
249
- const message = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : String(reason);
250
- buffer.pushMetric("__auto_js_error", userId, anonId, {
251
- value: 1,
252
- kind: "unhandled_rejection",
253
- message: message.slice(0, 200)
254
- });
255
- }
521
+ const reason = e.reason;
522
+ if (isExpected(reason)) return;
523
+ reportSee(
524
+ reason ?? "Unhandled promise rejection",
525
+ causesThe("page").to("hit an unhandled promise rejection"),
526
+ void 0,
527
+ "unhandled_rejection"
528
+ );
256
529
  });
257
530
  const origFetch = window.fetch;
258
531
  window.fetch = async function(...args) {
259
532
  const startedAt = typeof performance !== "undefined" ? performance.now() : 0;
260
533
  const url = typeof args[0] === "string" ? args[0] : args[0].toString();
534
+ const ignored = ignoreUrlPrefixes.some((p) => p && url.startsWith(p));
535
+ const bareUrl = url.split("?")[0].slice(0, 200);
261
536
  let res;
262
537
  try {
263
538
  res = await origFetch.apply(this, args);
264
539
  } catch (err) {
265
- if (netErrorCount < MAX_ERRORS_PER_SESSION) {
266
- netErrorCount += 1;
267
- buffer.pushMetric("__auto_network_error", userId, anonId, {
268
- value: 1,
269
- kind: "network",
270
- status: 0,
271
- url: url.slice(0, 200)
272
- });
540
+ if (!ignored && !isExpected(err)) {
541
+ reportSee(
542
+ violation("NetworkError").message(`request to ${bareUrl} failed`),
543
+ causesThe(`request to ${endpointTemplate(url)}`).to("get no response"),
544
+ { status: 0, url: url.slice(0, 200) },
545
+ "network"
546
+ );
273
547
  }
274
548
  throw err;
275
549
  }
276
- if (res.status >= 500 && netErrorCount < MAX_ERRORS_PER_SESSION) {
277
- netErrorCount += 1;
550
+ if (!ignored && res.status >= 500) {
278
551
  const elapsed = typeof performance !== "undefined" ? performance.now() - startedAt : 0;
279
- buffer.pushMetric("__auto_network_error", userId, anonId, {
280
- value: 1,
281
- kind: "5xx",
282
- status: res.status,
283
- url: url.slice(0, 200),
284
- duration_ms: Math.round(elapsed)
285
- });
552
+ reportSee(
553
+ violation("Http5xx").message(`request to ${bareUrl} returned ${res.status}`),
554
+ causesThe(`request to ${endpointTemplate(url)}`).to("fail with a server error"),
555
+ { status: res.status, url: url.slice(0, 200), duration_ms: Math.round(elapsed) },
556
+ "network"
557
+ );
286
558
  }
287
559
  return res;
288
560
  };
289
561
  }
290
562
  const flushNavTiming = () => {
291
563
  if (navTimingFlushed) return;
564
+ if (!groups.vitals) {
565
+ navTimingFlushed = true;
566
+ return;
567
+ }
568
+ if (!shouldEmit()) return;
292
569
  navTimingFlushed = true;
293
- if (!groups.vitals) return;
294
570
  try {
295
571
  const navList = performance.getEntriesByType("navigation");
296
572
  const nav = navList[0];
@@ -325,7 +601,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
325
601
  };
326
602
  if (groups.engagement) {
327
603
  try {
328
- buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
604
+ if (shouldEmit()) buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
329
605
  } catch {
330
606
  }
331
607
  let lastEmit = Date.now();
@@ -333,6 +609,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
333
609
  document.addEventListener("visibilitychange", () => {
334
610
  if (document.visibilityState !== "visible") return;
335
611
  if (Date.now() - lastEmit < SESSION_GAP_MS) return;
612
+ if (!shouldEmit()) return;
336
613
  try {
337
614
  buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
338
615
  lastEmit = Date.now();
@@ -355,7 +632,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
355
632
  }
356
633
  const flushOnHide = () => {
357
634
  flushNavTiming();
358
- if (groups.vitals) {
635
+ if (groups.vitals && shouldEmit()) {
359
636
  if (lcp !== null) buffer.pushMetric("__auto_lcp", userId, anonId, { value: lcp });
360
637
  if (inp !== null) buffer.pushMetric("__auto_inp", userId, anonId, { value: inp });
361
638
  if (clsBad) buffer.pushMetric("__auto_cls_binary", userId, anonId, { value: 1 });
@@ -419,6 +696,83 @@ function collectBrowserAttrs() {
419
696
  }
420
697
  return attrs;
421
698
  }
699
+ function collectSeeEnv() {
700
+ const out = {};
701
+ if (typeof navigator === "undefined") return out;
702
+ const nav = navigator;
703
+ const ua = typeof nav.userAgent === "string" ? nav.userAgent : "";
704
+ const browser = parseUaBrowser(ua);
705
+ if (browser) out["env.browser"] = browser;
706
+ const os = parseUaOs(ua) ?? nav.userAgentData?.platform;
707
+ if (os) out["env.os"] = os;
708
+ 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";
709
+ try {
710
+ if (nav.language) out["env.lang"] = nav.language;
711
+ } catch {
712
+ }
713
+ try {
714
+ if (typeof nav.onLine === "boolean") out["env.online"] = nav.onLine;
715
+ } catch {
716
+ }
717
+ try {
718
+ if (typeof nav.hardwareConcurrency === "number") out["env.cores"] = nav.hardwareConcurrency;
719
+ } catch {
720
+ }
721
+ try {
722
+ if (typeof nav.deviceMemory === "number") out["env.memory_gb"] = nav.deviceMemory;
723
+ } catch {
724
+ }
725
+ try {
726
+ const et = nav.connection?.effectiveType;
727
+ if (et) out["env.connection"] = et;
728
+ } catch {
729
+ }
730
+ try {
731
+ if (typeof window !== "undefined" && window.innerWidth && window.innerHeight) {
732
+ out["env.viewport"] = `${window.innerWidth}\xD7${window.innerHeight}`;
733
+ }
734
+ if (typeof window !== "undefined" && typeof window.devicePixelRatio === "number") {
735
+ out["env.dpr"] = window.devicePixelRatio;
736
+ }
737
+ if (typeof screen !== "undefined" && screen.width && screen.height) {
738
+ out["env.screen"] = `${screen.width}\xD7${screen.height}`;
739
+ }
740
+ } catch {
741
+ }
742
+ try {
743
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
744
+ if (tz) out["env.tz"] = tz;
745
+ } catch {
746
+ }
747
+ return out;
748
+ }
749
+ function parseUaBrowser(ua) {
750
+ const tests = [
751
+ [/Edg(?:A|iOS)?\/(\d+)/, "Edge"],
752
+ [/(?:OPR|Opera)\/(\d+)/, "Opera"],
753
+ [/(?:Firefox|FxiOS)\/(\d+)/, "Firefox"],
754
+ [/(?:Chrome|CriOS)\/(\d+)/, "Chrome"],
755
+ [/Version\/(\d+)[.\d]* (?:Mobile.*)?Safari/, "Safari"]
756
+ ];
757
+ for (const [re, name] of tests) {
758
+ const m = re.exec(ua);
759
+ if (m) return `${name} ${m[1]}`;
760
+ }
761
+ return void 0;
762
+ }
763
+ function parseUaOs(ua) {
764
+ if (/Windows NT 10/.test(ua)) return "Windows 10/11";
765
+ if (/Windows NT/.test(ua)) return "Windows";
766
+ let m = /Mac OS X (\d+)[._](\d+)/.exec(ua);
767
+ if (m) return `macOS ${m[1]}.${m[2]}`;
768
+ if (/Macintosh/.test(ua)) return "macOS";
769
+ m = /Android (\d+)/.exec(ua);
770
+ if (m) return `Android ${m[1]}`;
771
+ m = /(?:iPhone|iPad)[^)]* OS (\d+)/.exec(ua);
772
+ if (m) return `iOS ${m[1]}`;
773
+ if (/Linux/.test(ua)) return "Linux";
774
+ return void 0;
775
+ }
422
776
  function readExperimentOverridesFromUrl() {
423
777
  if (typeof window === "undefined") return {};
424
778
  const out = {};
@@ -438,12 +792,14 @@ var FlagsClientBrowser = class {
438
792
  baseUrl;
439
793
  autoGuardrails;
440
794
  autoGuardrailGroups;
795
+ autoCollectAlways;
441
796
  env;
442
797
  evalResult = null;
443
798
  anonId;
444
799
  userId = "";
445
800
  buffer;
446
801
  telemetry;
802
+ seeLimiter = new SeeLimiter();
447
803
  guardrailsInstalled = false;
448
804
  listeners = /* @__PURE__ */ new Set();
449
805
  overrideListenerInstalled = false;
@@ -459,6 +815,7 @@ var FlagsClientBrowser = class {
459
815
  this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
460
816
  this.env = opts.env ?? "prod";
461
817
  this.autoGuardrails = opts.autoGuardrails !== false;
818
+ this.autoCollectAlways = opts.autoCollectAlways === true;
462
819
  const g = opts.autoGuardrailGroups ?? {};
463
820
  this.autoGuardrailGroups = {
464
821
  vitals: g.vitals ?? this.autoGuardrails,
@@ -503,10 +860,39 @@ var FlagsClientBrowser = class {
503
860
  const anyGroupOn = this.autoGuardrailGroups.vitals || this.autoGuardrailGroups.errors || this.autoGuardrailGroups.engagement;
504
861
  if (anyGroupOn && !this.guardrailsInstalled) {
505
862
  this.guardrailsInstalled = true;
506
- installAutoGuardrails(this.buffer, this.userId, this.anonId, this.autoGuardrailGroups);
863
+ installAutoGuardrails(
864
+ this.buffer,
865
+ this.userId,
866
+ this.anonId,
867
+ this.autoGuardrailGroups,
868
+ (problem, consequence, extras, kind) => this.reportError(problem, consequence, extras, kind),
869
+ [`${this.baseUrl}/`, DEFAULT_TELEMETRY_URL],
870
+ this.autoCollectAlways
871
+ );
507
872
  }
508
873
  this.notify();
509
874
  }
875
+ /**
876
+ * Report a structured error into the errors primitive. Flushes immediately
877
+ * (beacon-first) — error occurrences are near-real-time, never queued behind
878
+ * the 5s metric batch. Spam-guarded by a 30s dedup window + per-session cap.
879
+ */
880
+ reportError(problem, consequence, extras, kind) {
881
+ try {
882
+ const enriched = { ...collectSeeEnv(), ...extras };
883
+ const ev = buildSeeEvent(problem, consequence, enriched, {
884
+ side: "client",
885
+ sdkVersion: version,
886
+ env: this.env,
887
+ url: typeof window !== "undefined" && window.location ? window.location.href : void 0,
888
+ userId: this.userId || void 0,
889
+ anonId: this.anonId
890
+ }, kind);
891
+ if (!this.seeLimiter.shouldSend(ev)) return;
892
+ this.buffer.sendNow([ev]);
893
+ } catch {
894
+ }
895
+ }
510
896
  get ready() {
511
897
  return this.evalResult !== null;
512
898
  }
@@ -738,13 +1124,15 @@ var _client = null;
738
1124
  function shipeasy(opts) {
739
1125
  const ac = opts.autoCollect;
740
1126
  const blanket = ac === false ? false : true;
741
- const groups = ac && typeof ac === "object" ? ac : void 0;
1127
+ const acObj = ac && typeof ac === "object" ? ac : void 0;
1128
+ const groups = acObj ? { vitals: acObj.vitals, errors: acObj.errors, engagement: acObj.engagement } : void 0;
742
1129
  const baseUrl = opts.baseUrl ?? "https://cdn.shipeasy.ai";
743
1130
  const client = configureShipeasy({
744
1131
  sdkKey: opts.clientKey,
745
1132
  baseUrl,
746
1133
  autoGuardrails: blanket,
747
1134
  autoGuardrailGroups: groups,
1135
+ autoCollectAlways: acObj?.always === true,
748
1136
  disableTelemetry: opts.disableTelemetry
749
1137
  });
750
1138
  injectI18nLoader(opts.clientKey, baseUrl, opts.i18nProfile);
@@ -906,6 +1294,20 @@ var flags = {
906
1294
  return _client?.ready ?? false;
907
1295
  }
908
1296
  };
1297
+ function dispatchSee(problem, consequence, extras, kind) {
1298
+ if (!_client) {
1299
+ console.warn("[shipeasy] see() called before shipeasy({ clientKey }) \u2014 error dropped");
1300
+ return;
1301
+ }
1302
+ _client.reportError(problem, consequence, extras, kind);
1303
+ }
1304
+ var see = Object.assign(
1305
+ (problem) => startSeeChain(() => problem, dispatchSee),
1306
+ {
1307
+ Violation: (name) => startSeeViolationChain(name, dispatchSee),
1308
+ ControlFlowException: markExpected
1309
+ }
1310
+ );
909
1311
  var LABEL_MARKER_START = "\uFFF9";
910
1312
  var LABEL_MARKER_SEP = "\uFFFA";
911
1313
  var LABEL_MARKER_END = "\uFFFB";
@@ -1157,6 +1559,7 @@ export {
1157
1559
  readConfigOverride,
1158
1560
  readExpOverride,
1159
1561
  readGateOverride,
1562
+ see,
1160
1563
  shipeasy,
1161
1564
  version
1162
1565
  };