@shipeasy/sdk 3.0.1 → 4.1.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.
@@ -1,5 +1,264 @@
1
+ // src/telemetry.ts
2
+ async function sha256Hex(input) {
3
+ const buf = new TextEncoder().encode(input);
4
+ const digest = await crypto.subtle.digest("SHA-256", buf);
5
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
6
+ }
7
+ var DEFAULT_TELEMETRY_URL = "https://t.shipeasy.ai";
8
+ var Telemetry = class {
9
+ prefix;
10
+ disabled;
11
+ dedupeMs;
12
+ // Last-emit timestamp per `feature/resource`, for the dedup window. Bounded by
13
+ // the number of distinct keys the app reads.
14
+ lastEmit = /* @__PURE__ */ new Map();
15
+ // Resolved once at construction and reused by every emit(), so the per-eval
16
+ // cost is a Map-free microtask, not a hash.
17
+ keyHash;
18
+ constructor(opts) {
19
+ const endpoint = (opts.endpoint ?? "").replace(/\/$/, "");
20
+ this.disabled = opts.disabled === true || !opts.sdkKey || !endpoint;
21
+ this.dedupeMs = opts.dedupeMs ?? 2e3;
22
+ this.prefix = `${endpoint}/t`;
23
+ this.keyHash = this.disabled ? null : sha256Hex(opts.sdkKey).then((h) => `${h}/${opts.side}/${encodeURIComponent(opts.env)}`).catch(() => "");
24
+ }
25
+ /**
26
+ * Emit a single best-effort usage beacon for one evaluation. Never blocks the
27
+ * caller (the hash is already resolved) and never throws — a failed beacon
28
+ * must never affect the evaluation it measures.
29
+ */
30
+ emit(feature, resource) {
31
+ if (this.disabled || !this.keyHash) return;
32
+ if (this.dedupeMs > 0) {
33
+ const dedupeKey = `${feature}/${resource}`;
34
+ const now = Date.now();
35
+ const last = this.lastEmit.get(dedupeKey);
36
+ if (last !== void 0 && now - last < this.dedupeMs) return;
37
+ this.lastEmit.set(dedupeKey, now);
38
+ }
39
+ void this.keyHash.then((suffix) => {
40
+ if (!suffix) return;
41
+ send(`${this.prefix}/${suffix}/${feature}/${encodeURIComponent(resource)}`);
42
+ });
43
+ }
44
+ };
45
+ function send(url) {
46
+ try {
47
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
48
+ navigator.sendBeacon(url);
49
+ return;
50
+ }
51
+ const f = globalThis.fetch;
52
+ if (typeof f === "function") {
53
+ void f(url, { method: "GET", keepalive: true }).catch(() => {
54
+ });
55
+ }
56
+ } catch {
57
+ }
58
+ }
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
+ function truncate(s, max) {
110
+ return s.length > max ? s.slice(0, max) : s;
111
+ }
112
+ function sanitizeExtras(extras) {
113
+ if (!extras || typeof extras !== "object") return void 0;
114
+ const out = {};
115
+ let n = 0;
116
+ for (const [k, v] of Object.entries(extras)) {
117
+ if (v === null || v === void 0) continue;
118
+ if (n >= SEE_MAX_EXTRA_KEYS) break;
119
+ if (typeof v === "string") out[k] = truncate(v, SEE_MAX_EXTRA_VALUE);
120
+ else if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
121
+ else if (typeof v === "boolean") out[k] = v;
122
+ else continue;
123
+ n += 1;
124
+ }
125
+ return n > 0 ? out : void 0;
126
+ }
127
+ function captureCallsiteStack() {
128
+ const raw = new Error().stack;
129
+ if (!raw) return void 0;
130
+ const lines = raw.split("\n");
131
+ const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
132
+ return kept.length ? kept.join("\n") : void 0;
133
+ }
134
+ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
135
+ let errorType;
136
+ let message;
137
+ let stack;
138
+ let kind;
139
+ if (isViolation(problem)) {
140
+ errorType = problem.violationName;
141
+ message = problem.violationMessage ?? problem.violationName;
142
+ stack = captureCallsiteStack();
143
+ kind = kindOverride ?? "violation";
144
+ } else if (problem instanceof Error) {
145
+ errorType = problem.name || "Error";
146
+ message = problem.message || String(problem);
147
+ stack = problem.stack ?? void 0;
148
+ kind = kindOverride ?? "caught";
149
+ } else {
150
+ errorType = "Error";
151
+ message = typeof problem === "string" ? problem : safeString(problem);
152
+ stack = captureCallsiteStack();
153
+ kind = kindOverride ?? "caught";
154
+ }
155
+ const ev = {
156
+ type: "error",
157
+ kind,
158
+ error_type: truncate(errorType, SEE_MAX_SUBJECT),
159
+ message: truncate(message, SEE_MAX_MESSAGE),
160
+ subject: consequence.subject,
161
+ outcome: consequence.outcome,
162
+ side: ctx.side,
163
+ sdk_version: ctx.sdkVersion,
164
+ ts: Date.now()
165
+ };
166
+ if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
167
+ const cleanExtras = sanitizeExtras(extras);
168
+ if (cleanExtras) ev.extras = cleanExtras;
169
+ if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
170
+ if (ctx.userId) ev.user_id = ctx.userId;
171
+ if (ctx.anonId) ev.anonymous_id = ctx.anonId;
172
+ if (ctx.env) ev.env = ctx.env;
173
+ return ev;
174
+ }
175
+ function safeString(v) {
176
+ try {
177
+ return typeof v === "object" ? JSON.stringify(v) : String(v);
178
+ } catch {
179
+ return String(v);
180
+ }
181
+ }
182
+ var scheduleMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : (cb) => {
183
+ void Promise.resolve().then(cb);
184
+ };
185
+ function startSeeChain(getProblem, dispatch) {
186
+ let subject;
187
+ let outcome;
188
+ let collected;
189
+ let flushed = false;
190
+ scheduleMicrotask(() => {
191
+ if (flushed) return;
192
+ flushed = true;
193
+ dispatch(
194
+ getProblem(),
195
+ causesThe(subject ?? "the app").to(outcome ?? "hit an error"),
196
+ collected
197
+ );
198
+ });
199
+ const tail = {
200
+ extras(x) {
201
+ if (x && typeof x === "object") collected = { ...collected, ...x };
202
+ return tail;
203
+ }
204
+ };
205
+ const step = {
206
+ to(o) {
207
+ outcome = String(o);
208
+ return tail;
209
+ }
210
+ };
211
+ const start = (s) => {
212
+ subject = String(s);
213
+ return step;
214
+ };
215
+ return { causes_the: start, causesThe: start };
216
+ }
217
+ function startSeeViolationChain(name, dispatch) {
218
+ let msg;
219
+ const base = startSeeChain(
220
+ () => msg !== void 0 ? violation(name).message(msg) : violation(name),
221
+ dispatch
222
+ );
223
+ const chain = {
224
+ ...base,
225
+ message(m) {
226
+ msg = String(m);
227
+ return chain;
228
+ }
229
+ };
230
+ return chain;
231
+ }
232
+ function topStackLine(stack) {
233
+ if (!stack) return "";
234
+ for (const line of stack.split("\n")) {
235
+ if (/^\s*at |@|:\d+:\d+/.test(line)) return line.trim().slice(0, 200);
236
+ }
237
+ return "";
238
+ }
239
+ var SeeLimiter = class {
240
+ constructor(maxPerSession = SEE_MAX_PER_SESSION, dedupWindowMs = SEE_DEDUP_WINDOW_MS) {
241
+ this.maxPerSession = maxPerSession;
242
+ this.dedupWindowMs = dedupWindowMs;
243
+ }
244
+ maxPerSession;
245
+ dedupWindowMs;
246
+ lastSent = /* @__PURE__ */ new Map();
247
+ sent = 0;
248
+ shouldSend(ev) {
249
+ if (this.sent >= this.maxPerSession) return false;
250
+ const key = `${ev.kind}|${ev.error_type}|${ev.message.slice(0, 200)}|${topStackLine(ev.stack)}`;
251
+ const now = Date.now();
252
+ const prev = this.lastSent.get(key);
253
+ if (prev !== void 0 && now - prev < this.dedupWindowMs) return false;
254
+ this.lastSent.set(key, now);
255
+ this.sent += 1;
256
+ return true;
257
+ }
258
+ };
259
+
1
260
  // src/client/index.ts
2
- var version = "1.0.0";
261
+ var version = "4.0.0";
3
262
  var FLUSH_INTERVAL_MS = 5e3;
4
263
  var MAX_BUFFER = 100;
5
264
  var ANON_ID_KEY = "__se_anon_id";
@@ -33,6 +292,13 @@ var EventBuffer = class {
33
292
  this.timer = null;
34
293
  }
35
294
  }
295
+ /** True once this visitor has been exposed to ≥1 experiment (this tab or a
296
+ * prior page in the session — the dedup set persists in sessionStorage).
297
+ * Gates auto-metric emission: vitals from non-participants are never read
298
+ * by the analysis pipeline and would be pure AE write cost (see cost.md). */
299
+ hasExposures() {
300
+ return this.exposureSeen.size > 0;
301
+ }
36
302
  pushExposure(experiment, group, userId, anonId) {
37
303
  const key = `${userId || anonId}:${experiment}`;
38
304
  if (this.exposureSeen.has(key)) return;
@@ -98,16 +364,29 @@ var EventBuffer = class {
98
364
  flush(useBeacon = false) {
99
365
  if (!this.queue.length) return;
100
366
  const batch = this.queue.splice(0);
101
- const body = JSON.stringify({ events: batch });
367
+ this.send(batch, useBeacon);
368
+ }
369
+ /**
370
+ * Bypass the 5s queue and ship events immediately — used by see() error
371
+ * reporting so occurrences land near-real-time and survive page unload.
372
+ * Beacon-first (fire-and-forget, unload-safe), keepalive fetch fallback.
373
+ */
374
+ sendNow(events) {
375
+ this.send(events, true);
376
+ }
377
+ send(batch, useBeacon) {
102
378
  if (useBeacon && typeof navigator !== "undefined" && navigator.sendBeacon) {
103
379
  const beaconBody = JSON.stringify({ k: this.sdkKey, events: batch });
104
- navigator.sendBeacon(this.collectUrl, new Blob([beaconBody], { type: "text/plain" }));
105
- return;
380
+ try {
381
+ if (navigator.sendBeacon(this.collectUrl, new Blob([beaconBody], { type: "text/plain" })))
382
+ return;
383
+ } catch {
384
+ }
106
385
  }
107
386
  fetch(this.collectUrl, {
108
387
  method: "POST",
109
388
  headers: { "X-SDK-Key": this.sdkKey, "Content-Type": "application/json" },
110
- body,
389
+ body: JSON.stringify({ events: batch }),
111
390
  keepalive: true
112
391
  }).catch(() => {
113
392
  });
@@ -124,14 +403,12 @@ var EventBuffer = class {
124
403
  });
125
404
  }
126
405
  };
127
- var MAX_ERRORS_PER_SESSION = 5;
128
- function installAutoGuardrails(buffer, userId, anonId, groups) {
406
+ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignoreUrlPrefixes, always = false) {
129
407
  if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") return;
408
+ const shouldEmit = () => always || buffer.hasExposures();
130
409
  let lcp = null;
131
410
  let inp = null;
132
411
  let clsBad = false;
133
- let jsErrorCount = 0;
134
- let netErrorCount = 0;
135
412
  let navTimingFlushed = false;
136
413
  if (groups.vitals) {
137
414
  try {
@@ -170,68 +447,71 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
170
447
  if (groups.errors) {
171
448
  const origOnError = window.onerror;
172
449
  window.onerror = (msg, source, lineno, _colno, err) => {
173
- if (jsErrorCount < MAX_ERRORS_PER_SESSION) {
174
- jsErrorCount += 1;
175
- buffer.pushMetric("__auto_js_error", userId, anonId, {
176
- value: 1,
177
- kind: "exception",
178
- message: typeof msg === "string" ? msg.slice(0, 200) : String(err ?? "").slice(0, 200),
179
- source: typeof source === "string" ? source.slice(0, 200) : "",
180
- line: lineno ?? 0
181
- });
450
+ if (!isExpected(err)) {
451
+ const problem = err ?? (typeof msg === "string" && msg ? msg : "Unknown error");
452
+ reportSee(
453
+ problem,
454
+ causesThe("the page").to("hit an unhandled error"),
455
+ {
456
+ source: typeof source === "string" ? source : void 0,
457
+ line: lineno ?? void 0
458
+ },
459
+ "uncaught"
460
+ );
182
461
  }
183
462
  if (typeof origOnError === "function") return origOnError(msg, source, lineno, _colno, err);
184
463
  return false;
185
464
  };
186
465
  window.addEventListener("unhandledrejection", (e) => {
187
- if (jsErrorCount < MAX_ERRORS_PER_SESSION) {
188
- jsErrorCount += 1;
189
- const reason = e.reason;
190
- const message = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : String(reason);
191
- buffer.pushMetric("__auto_js_error", userId, anonId, {
192
- value: 1,
193
- kind: "unhandled_rejection",
194
- message: message.slice(0, 200)
195
- });
196
- }
466
+ const reason = e.reason;
467
+ if (isExpected(reason)) return;
468
+ reportSee(
469
+ reason ?? "Unhandled promise rejection",
470
+ causesThe("the page").to("hit an unhandled promise rejection"),
471
+ void 0,
472
+ "unhandled_rejection"
473
+ );
197
474
  });
198
475
  const origFetch = window.fetch;
199
476
  window.fetch = async function(...args) {
200
477
  const startedAt = typeof performance !== "undefined" ? performance.now() : 0;
201
478
  const url = typeof args[0] === "string" ? args[0] : args[0].toString();
479
+ const ignored = ignoreUrlPrefixes.some((p) => p && url.startsWith(p));
480
+ const bareUrl = url.split("?")[0].slice(0, 200);
202
481
  let res;
203
482
  try {
204
483
  res = await origFetch.apply(this, args);
205
484
  } catch (err) {
206
- if (netErrorCount < MAX_ERRORS_PER_SESSION) {
207
- netErrorCount += 1;
208
- buffer.pushMetric("__auto_network_error", userId, anonId, {
209
- value: 1,
210
- kind: "network",
211
- status: 0,
212
- url: url.slice(0, 200)
213
- });
485
+ if (!ignored && !isExpected(err)) {
486
+ reportSee(
487
+ violation("NetworkError").message(`request to ${bareUrl} failed`),
488
+ causesThe("a network request").to("fail without a response"),
489
+ { status: 0, url: url.slice(0, 200) },
490
+ "network"
491
+ );
214
492
  }
215
493
  throw err;
216
494
  }
217
- if (res.status >= 500 && netErrorCount < MAX_ERRORS_PER_SESSION) {
218
- netErrorCount += 1;
495
+ if (!ignored && res.status >= 500) {
219
496
  const elapsed = typeof performance !== "undefined" ? performance.now() - startedAt : 0;
220
- buffer.pushMetric("__auto_network_error", userId, anonId, {
221
- value: 1,
222
- kind: "5xx",
223
- status: res.status,
224
- url: url.slice(0, 200),
225
- duration_ms: Math.round(elapsed)
226
- });
497
+ reportSee(
498
+ violation("Http5xx").message(`request to ${bareUrl} returned ${res.status}`),
499
+ causesThe("a network request").to(`fail with HTTP ${res.status}`),
500
+ { status: res.status, url: url.slice(0, 200), duration_ms: Math.round(elapsed) },
501
+ "network"
502
+ );
227
503
  }
228
504
  return res;
229
505
  };
230
506
  }
231
507
  const flushNavTiming = () => {
232
508
  if (navTimingFlushed) return;
509
+ if (!groups.vitals) {
510
+ navTimingFlushed = true;
511
+ return;
512
+ }
513
+ if (!shouldEmit()) return;
233
514
  navTimingFlushed = true;
234
- if (!groups.vitals) return;
235
515
  try {
236
516
  const navList = performance.getEntriesByType("navigation");
237
517
  const nav = navList[0];
@@ -266,7 +546,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
266
546
  };
267
547
  if (groups.engagement) {
268
548
  try {
269
- buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
549
+ if (shouldEmit()) buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
270
550
  } catch {
271
551
  }
272
552
  let lastEmit = Date.now();
@@ -274,6 +554,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
274
554
  document.addEventListener("visibilitychange", () => {
275
555
  if (document.visibilityState !== "visible") return;
276
556
  if (Date.now() - lastEmit < SESSION_GAP_MS) return;
557
+ if (!shouldEmit()) return;
277
558
  try {
278
559
  buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
279
560
  lastEmit = Date.now();
@@ -296,7 +577,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
296
577
  }
297
578
  const flushOnHide = () => {
298
579
  flushNavTiming();
299
- if (groups.vitals) {
580
+ if (groups.vitals && shouldEmit()) {
300
581
  if (lcp !== null) buffer.pushMetric("__auto_lcp", userId, anonId, { value: lcp });
301
582
  if (inp !== null) buffer.pushMetric("__auto_inp", userId, anonId, { value: inp });
302
583
  if (clsBad) buffer.pushMetric("__auto_cls_binary", userId, anonId, { value: 1 });
@@ -379,11 +660,14 @@ var FlagsClientBrowser = class {
379
660
  baseUrl;
380
661
  autoGuardrails;
381
662
  autoGuardrailGroups;
663
+ autoCollectAlways;
382
664
  env;
383
665
  evalResult = null;
384
666
  anonId;
385
667
  userId = "";
386
668
  buffer;
669
+ telemetry;
670
+ seeLimiter = new SeeLimiter();
387
671
  guardrailsInstalled = false;
388
672
  listeners = /* @__PURE__ */ new Set();
389
673
  overrideListenerInstalled = false;
@@ -399,6 +683,7 @@ var FlagsClientBrowser = class {
399
683
  this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
400
684
  this.env = opts.env ?? "prod";
401
685
  this.autoGuardrails = opts.autoGuardrails !== false;
686
+ this.autoCollectAlways = opts.autoCollectAlways === true;
402
687
  const g = opts.autoGuardrailGroups ?? {};
403
688
  this.autoGuardrailGroups = {
404
689
  vitals: g.vitals ?? this.autoGuardrails,
@@ -407,6 +692,13 @@ var FlagsClientBrowser = class {
407
692
  };
408
693
  this.anonId = getOrCreateAnonId();
409
694
  this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
695
+ this.telemetry = new Telemetry({
696
+ endpoint: opts.telemetryUrl ?? DEFAULT_TELEMETRY_URL,
697
+ sdkKey: this.sdkKey,
698
+ side: "client",
699
+ env: this.env,
700
+ disabled: opts.disableTelemetry
701
+ });
410
702
  void this.buffer.flushPendingAlias();
411
703
  }
412
704
  async identify(user) {
@@ -436,10 +728,38 @@ var FlagsClientBrowser = class {
436
728
  const anyGroupOn = this.autoGuardrailGroups.vitals || this.autoGuardrailGroups.errors || this.autoGuardrailGroups.engagement;
437
729
  if (anyGroupOn && !this.guardrailsInstalled) {
438
730
  this.guardrailsInstalled = true;
439
- installAutoGuardrails(this.buffer, this.userId, this.anonId, this.autoGuardrailGroups);
731
+ installAutoGuardrails(
732
+ this.buffer,
733
+ this.userId,
734
+ this.anonId,
735
+ this.autoGuardrailGroups,
736
+ (problem, consequence, extras, kind) => this.reportError(problem, consequence, extras, kind),
737
+ [`${this.baseUrl}/`, DEFAULT_TELEMETRY_URL],
738
+ this.autoCollectAlways
739
+ );
440
740
  }
441
741
  this.notify();
442
742
  }
743
+ /**
744
+ * Report a structured error into the errors primitive. Flushes immediately
745
+ * (beacon-first) — error occurrences are near-real-time, never queued behind
746
+ * the 5s metric batch. Spam-guarded by a 30s dedup window + per-session cap.
747
+ */
748
+ reportError(problem, consequence, extras, kind) {
749
+ try {
750
+ const ev = buildSeeEvent(problem, consequence, extras, {
751
+ side: "client",
752
+ sdkVersion: version,
753
+ env: this.env,
754
+ url: typeof window !== "undefined" && window.location ? window.location.href : void 0,
755
+ userId: this.userId || void 0,
756
+ anonId: this.anonId
757
+ }, kind);
758
+ if (!this.seeLimiter.shouldSend(ev)) return;
759
+ this.buffer.sendNow([ev]);
760
+ } catch {
761
+ }
762
+ }
443
763
  get ready() {
444
764
  return this.evalResult !== null;
445
765
  }
@@ -456,12 +776,14 @@ var FlagsClientBrowser = class {
456
776
  this.evalResult = data;
457
777
  }
458
778
  getFlag(name) {
779
+ this.telemetry.emit("gate", name);
459
780
  if (this.evalResult === null) return false;
460
781
  const ov = readGateOverride(name);
461
782
  if (ov !== null) return ov;
462
783
  return this.evalResult.flags[name] ?? false;
463
784
  }
464
785
  getConfig(name, decode) {
786
+ this.telemetry.emit("config", name);
465
787
  if (this.evalResult === null) return void 0;
466
788
  const ov = readConfigOverride(name);
467
789
  const raw = ov !== void 0 ? ov : this.evalResult.configs?.[name];
@@ -475,6 +797,7 @@ var FlagsClientBrowser = class {
475
797
  }
476
798
  }
477
799
  getExperiment(name, defaultParams, decode, variants) {
800
+ this.telemetry.emit("experiment", name);
478
801
  const notIn = {
479
802
  inExperiment: false,
480
803
  group: "control",
@@ -539,6 +862,7 @@ var FlagsClientBrowser = class {
539
862
  * the per-switch state. Returns false for unknown killswitches / switches.
540
863
  */
541
864
  getKillswitch(name, switchKey) {
865
+ this.telemetry.emit("ks", name);
542
866
  if (this.evalResult === null) return false;
543
867
  const ks = this.evalResult.killswitches?.[name];
544
868
  if (ks === void 0) return false;
@@ -667,13 +991,16 @@ var _client = null;
667
991
  function shipeasy(opts) {
668
992
  const ac = opts.autoCollect;
669
993
  const blanket = ac === false ? false : true;
670
- const groups = ac && typeof ac === "object" ? ac : void 0;
994
+ const acObj = ac && typeof ac === "object" ? ac : void 0;
995
+ const groups = acObj ? { vitals: acObj.vitals, errors: acObj.errors, engagement: acObj.engagement } : void 0;
671
996
  const baseUrl = opts.baseUrl ?? "https://cdn.shipeasy.ai";
672
997
  const client = configureShipeasy({
673
998
  sdkKey: opts.clientKey,
674
999
  baseUrl,
675
1000
  autoGuardrails: blanket,
676
- autoGuardrailGroups: groups
1001
+ autoGuardrailGroups: groups,
1002
+ autoCollectAlways: acObj?.always === true,
1003
+ disableTelemetry: opts.disableTelemetry
677
1004
  });
678
1005
  injectI18nLoader(opts.clientKey, baseUrl, opts.i18nProfile);
679
1006
  flags.notifyMounted();
@@ -834,6 +1161,20 @@ var flags = {
834
1161
  return _client?.ready ?? false;
835
1162
  }
836
1163
  };
1164
+ function dispatchSee(problem, consequence, extras, kind) {
1165
+ if (!_client) {
1166
+ console.warn("[shipeasy] see() called before shipeasy({ clientKey }) \u2014 error dropped");
1167
+ return;
1168
+ }
1169
+ _client.reportError(problem, consequence, extras, kind);
1170
+ }
1171
+ var see = Object.assign(
1172
+ (problem) => startSeeChain(() => problem, dispatchSee),
1173
+ {
1174
+ Violation: (name) => startSeeViolationChain(name, dispatchSee),
1175
+ ControlFlowException: markExpected
1176
+ }
1177
+ );
837
1178
  var LABEL_MARKER_START = "\uFFF9";
838
1179
  var LABEL_MARKER_SEP = "\uFFFA";
839
1180
  var LABEL_MARKER_END = "\uFFFB";
@@ -1085,6 +1426,7 @@ export {
1085
1426
  readConfigOverride,
1086
1427
  readExpOverride,
1087
1428
  readGateOverride,
1429
+ see,
1088
1430
  shipeasy,
1089
1431
  version
1090
1432
  };