@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.
@@ -38,11 +38,273 @@ __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
  });
44
45
  module.exports = __toCommonJS(client_exports);
45
- var version = "1.0.0";
46
+
47
+ // src/telemetry.ts
48
+ async function sha256Hex(input) {
49
+ const buf = new TextEncoder().encode(input);
50
+ const digest = await crypto.subtle.digest("SHA-256", buf);
51
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
52
+ }
53
+ var DEFAULT_TELEMETRY_URL = "https://t.shipeasy.ai";
54
+ var Telemetry = class {
55
+ prefix;
56
+ disabled;
57
+ dedupeMs;
58
+ // Last-emit timestamp per `feature/resource`, for the dedup window. Bounded by
59
+ // the number of distinct keys the app reads.
60
+ lastEmit = /* @__PURE__ */ new Map();
61
+ // Resolved once at construction and reused by every emit(), so the per-eval
62
+ // cost is a Map-free microtask, not a hash.
63
+ keyHash;
64
+ constructor(opts) {
65
+ const endpoint = (opts.endpoint ?? "").replace(/\/$/, "");
66
+ this.disabled = opts.disabled === true || !opts.sdkKey || !endpoint;
67
+ this.dedupeMs = opts.dedupeMs ?? 2e3;
68
+ this.prefix = `${endpoint}/t`;
69
+ this.keyHash = this.disabled ? null : sha256Hex(opts.sdkKey).then((h) => `${h}/${opts.side}/${encodeURIComponent(opts.env)}`).catch(() => "");
70
+ }
71
+ /**
72
+ * Emit a single best-effort usage beacon for one evaluation. Never blocks the
73
+ * caller (the hash is already resolved) and never throws — a failed beacon
74
+ * must never affect the evaluation it measures.
75
+ */
76
+ emit(feature, resource) {
77
+ if (this.disabled || !this.keyHash) return;
78
+ if (this.dedupeMs > 0) {
79
+ const dedupeKey = `${feature}/${resource}`;
80
+ const now = Date.now();
81
+ const last = this.lastEmit.get(dedupeKey);
82
+ if (last !== void 0 && now - last < this.dedupeMs) return;
83
+ this.lastEmit.set(dedupeKey, now);
84
+ }
85
+ void this.keyHash.then((suffix) => {
86
+ if (!suffix) return;
87
+ send(`${this.prefix}/${suffix}/${feature}/${encodeURIComponent(resource)}`);
88
+ });
89
+ }
90
+ };
91
+ function send(url) {
92
+ try {
93
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
94
+ navigator.sendBeacon(url);
95
+ return;
96
+ }
97
+ const f = globalThis.fetch;
98
+ if (typeof f === "function") {
99
+ void f(url, { method: "GET", keepalive: true }).catch(() => {
100
+ });
101
+ }
102
+ } catch {
103
+ }
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
+ function truncate(s, max) {
156
+ return s.length > max ? s.slice(0, max) : s;
157
+ }
158
+ function sanitizeExtras(extras) {
159
+ if (!extras || typeof extras !== "object") return void 0;
160
+ const out = {};
161
+ let n = 0;
162
+ for (const [k, v] of Object.entries(extras)) {
163
+ if (v === null || v === void 0) continue;
164
+ if (n >= SEE_MAX_EXTRA_KEYS) break;
165
+ if (typeof v === "string") out[k] = truncate(v, SEE_MAX_EXTRA_VALUE);
166
+ else if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
167
+ else if (typeof v === "boolean") out[k] = v;
168
+ else continue;
169
+ n += 1;
170
+ }
171
+ return n > 0 ? out : void 0;
172
+ }
173
+ function captureCallsiteStack() {
174
+ const raw = new Error().stack;
175
+ if (!raw) return void 0;
176
+ const lines = raw.split("\n");
177
+ const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
178
+ return kept.length ? kept.join("\n") : void 0;
179
+ }
180
+ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
181
+ let errorType;
182
+ let message;
183
+ let stack;
184
+ let kind;
185
+ if (isViolation(problem)) {
186
+ errorType = problem.violationName;
187
+ message = problem.violationMessage ?? problem.violationName;
188
+ stack = captureCallsiteStack();
189
+ kind = kindOverride ?? "violation";
190
+ } else if (problem instanceof Error) {
191
+ errorType = problem.name || "Error";
192
+ message = problem.message || String(problem);
193
+ stack = problem.stack ?? void 0;
194
+ kind = kindOverride ?? "caught";
195
+ } else {
196
+ errorType = "Error";
197
+ message = typeof problem === "string" ? problem : safeString(problem);
198
+ stack = captureCallsiteStack();
199
+ kind = kindOverride ?? "caught";
200
+ }
201
+ const ev = {
202
+ type: "error",
203
+ kind,
204
+ error_type: truncate(errorType, SEE_MAX_SUBJECT),
205
+ message: truncate(message, SEE_MAX_MESSAGE),
206
+ subject: consequence.subject,
207
+ outcome: consequence.outcome,
208
+ side: ctx.side,
209
+ sdk_version: ctx.sdkVersion,
210
+ ts: Date.now()
211
+ };
212
+ if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
213
+ const cleanExtras = sanitizeExtras(extras);
214
+ if (cleanExtras) ev.extras = cleanExtras;
215
+ if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
216
+ if (ctx.userId) ev.user_id = ctx.userId;
217
+ if (ctx.anonId) ev.anonymous_id = ctx.anonId;
218
+ if (ctx.env) ev.env = ctx.env;
219
+ return ev;
220
+ }
221
+ function safeString(v) {
222
+ try {
223
+ return typeof v === "object" ? JSON.stringify(v) : String(v);
224
+ } catch {
225
+ return String(v);
226
+ }
227
+ }
228
+ var scheduleMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : (cb) => {
229
+ void Promise.resolve().then(cb);
230
+ };
231
+ function startSeeChain(getProblem, dispatch) {
232
+ let subject;
233
+ let outcome;
234
+ let collected;
235
+ let flushed = false;
236
+ scheduleMicrotask(() => {
237
+ if (flushed) return;
238
+ flushed = true;
239
+ dispatch(
240
+ getProblem(),
241
+ causesThe(subject ?? "the app").to(outcome ?? "hit an error"),
242
+ collected
243
+ );
244
+ });
245
+ const tail = {
246
+ extras(x) {
247
+ if (x && typeof x === "object") collected = { ...collected, ...x };
248
+ return tail;
249
+ }
250
+ };
251
+ const step = {
252
+ to(o) {
253
+ outcome = String(o);
254
+ return tail;
255
+ }
256
+ };
257
+ const start = (s) => {
258
+ subject = String(s);
259
+ return step;
260
+ };
261
+ return { causes_the: start, causesThe: start };
262
+ }
263
+ function startSeeViolationChain(name, dispatch) {
264
+ let msg;
265
+ const base = startSeeChain(
266
+ () => msg !== void 0 ? violation(name).message(msg) : violation(name),
267
+ dispatch
268
+ );
269
+ const chain = {
270
+ ...base,
271
+ message(m) {
272
+ msg = String(m);
273
+ return chain;
274
+ }
275
+ };
276
+ return chain;
277
+ }
278
+ function topStackLine(stack) {
279
+ if (!stack) return "";
280
+ for (const line of stack.split("\n")) {
281
+ if (/^\s*at |@|:\d+:\d+/.test(line)) return line.trim().slice(0, 200);
282
+ }
283
+ return "";
284
+ }
285
+ var SeeLimiter = class {
286
+ constructor(maxPerSession = SEE_MAX_PER_SESSION, dedupWindowMs = SEE_DEDUP_WINDOW_MS) {
287
+ this.maxPerSession = maxPerSession;
288
+ this.dedupWindowMs = dedupWindowMs;
289
+ }
290
+ maxPerSession;
291
+ dedupWindowMs;
292
+ lastSent = /* @__PURE__ */ new Map();
293
+ sent = 0;
294
+ shouldSend(ev) {
295
+ if (this.sent >= this.maxPerSession) return false;
296
+ const key = `${ev.kind}|${ev.error_type}|${ev.message.slice(0, 200)}|${topStackLine(ev.stack)}`;
297
+ const now = Date.now();
298
+ const prev = this.lastSent.get(key);
299
+ if (prev !== void 0 && now - prev < this.dedupWindowMs) return false;
300
+ this.lastSent.set(key, now);
301
+ this.sent += 1;
302
+ return true;
303
+ }
304
+ };
305
+
306
+ // src/client/index.ts
307
+ var version = "4.0.0";
46
308
  var FLUSH_INTERVAL_MS = 5e3;
47
309
  var MAX_BUFFER = 100;
48
310
  var ANON_ID_KEY = "__se_anon_id";
@@ -76,6 +338,13 @@ var EventBuffer = class {
76
338
  this.timer = null;
77
339
  }
78
340
  }
341
+ /** True once this visitor has been exposed to ≥1 experiment (this tab or a
342
+ * prior page in the session — the dedup set persists in sessionStorage).
343
+ * Gates auto-metric emission: vitals from non-participants are never read
344
+ * by the analysis pipeline and would be pure AE write cost (see cost.md). */
345
+ hasExposures() {
346
+ return this.exposureSeen.size > 0;
347
+ }
79
348
  pushExposure(experiment, group, userId, anonId) {
80
349
  const key = `${userId || anonId}:${experiment}`;
81
350
  if (this.exposureSeen.has(key)) return;
@@ -141,16 +410,29 @@ var EventBuffer = class {
141
410
  flush(useBeacon = false) {
142
411
  if (!this.queue.length) return;
143
412
  const batch = this.queue.splice(0);
144
- const body = JSON.stringify({ events: batch });
413
+ this.send(batch, useBeacon);
414
+ }
415
+ /**
416
+ * Bypass the 5s queue and ship events immediately — used by see() error
417
+ * reporting so occurrences land near-real-time and survive page unload.
418
+ * Beacon-first (fire-and-forget, unload-safe), keepalive fetch fallback.
419
+ */
420
+ sendNow(events) {
421
+ this.send(events, true);
422
+ }
423
+ send(batch, useBeacon) {
145
424
  if (useBeacon && typeof navigator !== "undefined" && navigator.sendBeacon) {
146
425
  const beaconBody = JSON.stringify({ k: this.sdkKey, events: batch });
147
- navigator.sendBeacon(this.collectUrl, new Blob([beaconBody], { type: "text/plain" }));
148
- return;
426
+ try {
427
+ if (navigator.sendBeacon(this.collectUrl, new Blob([beaconBody], { type: "text/plain" })))
428
+ return;
429
+ } catch {
430
+ }
149
431
  }
150
432
  fetch(this.collectUrl, {
151
433
  method: "POST",
152
434
  headers: { "X-SDK-Key": this.sdkKey, "Content-Type": "application/json" },
153
- body,
435
+ body: JSON.stringify({ events: batch }),
154
436
  keepalive: true
155
437
  }).catch(() => {
156
438
  });
@@ -167,14 +449,12 @@ var EventBuffer = class {
167
449
  });
168
450
  }
169
451
  };
170
- var MAX_ERRORS_PER_SESSION = 5;
171
- function installAutoGuardrails(buffer, userId, anonId, groups) {
452
+ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignoreUrlPrefixes, always = false) {
172
453
  if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") return;
454
+ const shouldEmit = () => always || buffer.hasExposures();
173
455
  let lcp = null;
174
456
  let inp = null;
175
457
  let clsBad = false;
176
- let jsErrorCount = 0;
177
- let netErrorCount = 0;
178
458
  let navTimingFlushed = false;
179
459
  if (groups.vitals) {
180
460
  try {
@@ -213,68 +493,71 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
213
493
  if (groups.errors) {
214
494
  const origOnError = window.onerror;
215
495
  window.onerror = (msg, source, lineno, _colno, err) => {
216
- if (jsErrorCount < MAX_ERRORS_PER_SESSION) {
217
- jsErrorCount += 1;
218
- buffer.pushMetric("__auto_js_error", userId, anonId, {
219
- value: 1,
220
- kind: "exception",
221
- message: typeof msg === "string" ? msg.slice(0, 200) : String(err ?? "").slice(0, 200),
222
- source: typeof source === "string" ? source.slice(0, 200) : "",
223
- line: lineno ?? 0
224
- });
496
+ if (!isExpected(err)) {
497
+ const problem = err ?? (typeof msg === "string" && msg ? msg : "Unknown error");
498
+ reportSee(
499
+ problem,
500
+ causesThe("the page").to("hit an unhandled error"),
501
+ {
502
+ source: typeof source === "string" ? source : void 0,
503
+ line: lineno ?? void 0
504
+ },
505
+ "uncaught"
506
+ );
225
507
  }
226
508
  if (typeof origOnError === "function") return origOnError(msg, source, lineno, _colno, err);
227
509
  return false;
228
510
  };
229
511
  window.addEventListener("unhandledrejection", (e) => {
230
- if (jsErrorCount < MAX_ERRORS_PER_SESSION) {
231
- jsErrorCount += 1;
232
- const reason = e.reason;
233
- const message = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : String(reason);
234
- buffer.pushMetric("__auto_js_error", userId, anonId, {
235
- value: 1,
236
- kind: "unhandled_rejection",
237
- message: message.slice(0, 200)
238
- });
239
- }
512
+ const reason = e.reason;
513
+ if (isExpected(reason)) return;
514
+ reportSee(
515
+ reason ?? "Unhandled promise rejection",
516
+ causesThe("the page").to("hit an unhandled promise rejection"),
517
+ void 0,
518
+ "unhandled_rejection"
519
+ );
240
520
  });
241
521
  const origFetch = window.fetch;
242
522
  window.fetch = async function(...args) {
243
523
  const startedAt = typeof performance !== "undefined" ? performance.now() : 0;
244
524
  const url = typeof args[0] === "string" ? args[0] : args[0].toString();
525
+ const ignored = ignoreUrlPrefixes.some((p) => p && url.startsWith(p));
526
+ const bareUrl = url.split("?")[0].slice(0, 200);
245
527
  let res;
246
528
  try {
247
529
  res = await origFetch.apply(this, args);
248
530
  } catch (err) {
249
- if (netErrorCount < MAX_ERRORS_PER_SESSION) {
250
- netErrorCount += 1;
251
- buffer.pushMetric("__auto_network_error", userId, anonId, {
252
- value: 1,
253
- kind: "network",
254
- status: 0,
255
- url: url.slice(0, 200)
256
- });
531
+ if (!ignored && !isExpected(err)) {
532
+ reportSee(
533
+ violation("NetworkError").message(`request to ${bareUrl} failed`),
534
+ causesThe("a network request").to("fail without a response"),
535
+ { status: 0, url: url.slice(0, 200) },
536
+ "network"
537
+ );
257
538
  }
258
539
  throw err;
259
540
  }
260
- if (res.status >= 500 && netErrorCount < MAX_ERRORS_PER_SESSION) {
261
- netErrorCount += 1;
541
+ if (!ignored && res.status >= 500) {
262
542
  const elapsed = typeof performance !== "undefined" ? performance.now() - startedAt : 0;
263
- buffer.pushMetric("__auto_network_error", userId, anonId, {
264
- value: 1,
265
- kind: "5xx",
266
- status: res.status,
267
- url: url.slice(0, 200),
268
- duration_ms: Math.round(elapsed)
269
- });
543
+ reportSee(
544
+ violation("Http5xx").message(`request to ${bareUrl} returned ${res.status}`),
545
+ causesThe("a network request").to(`fail with HTTP ${res.status}`),
546
+ { status: res.status, url: url.slice(0, 200), duration_ms: Math.round(elapsed) },
547
+ "network"
548
+ );
270
549
  }
271
550
  return res;
272
551
  };
273
552
  }
274
553
  const flushNavTiming = () => {
275
554
  if (navTimingFlushed) return;
555
+ if (!groups.vitals) {
556
+ navTimingFlushed = true;
557
+ return;
558
+ }
559
+ if (!shouldEmit()) return;
276
560
  navTimingFlushed = true;
277
- if (!groups.vitals) return;
278
561
  try {
279
562
  const navList = performance.getEntriesByType("navigation");
280
563
  const nav = navList[0];
@@ -309,7 +592,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
309
592
  };
310
593
  if (groups.engagement) {
311
594
  try {
312
- buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
595
+ if (shouldEmit()) buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
313
596
  } catch {
314
597
  }
315
598
  let lastEmit = Date.now();
@@ -317,6 +600,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
317
600
  document.addEventListener("visibilitychange", () => {
318
601
  if (document.visibilityState !== "visible") return;
319
602
  if (Date.now() - lastEmit < SESSION_GAP_MS) return;
603
+ if (!shouldEmit()) return;
320
604
  try {
321
605
  buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
322
606
  lastEmit = Date.now();
@@ -339,7 +623,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
339
623
  }
340
624
  const flushOnHide = () => {
341
625
  flushNavTiming();
342
- if (groups.vitals) {
626
+ if (groups.vitals && shouldEmit()) {
343
627
  if (lcp !== null) buffer.pushMetric("__auto_lcp", userId, anonId, { value: lcp });
344
628
  if (inp !== null) buffer.pushMetric("__auto_inp", userId, anonId, { value: inp });
345
629
  if (clsBad) buffer.pushMetric("__auto_cls_binary", userId, anonId, { value: 1 });
@@ -422,11 +706,14 @@ var FlagsClientBrowser = class {
422
706
  baseUrl;
423
707
  autoGuardrails;
424
708
  autoGuardrailGroups;
709
+ autoCollectAlways;
425
710
  env;
426
711
  evalResult = null;
427
712
  anonId;
428
713
  userId = "";
429
714
  buffer;
715
+ telemetry;
716
+ seeLimiter = new SeeLimiter();
430
717
  guardrailsInstalled = false;
431
718
  listeners = /* @__PURE__ */ new Set();
432
719
  overrideListenerInstalled = false;
@@ -442,6 +729,7 @@ var FlagsClientBrowser = class {
442
729
  this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
443
730
  this.env = opts.env ?? "prod";
444
731
  this.autoGuardrails = opts.autoGuardrails !== false;
732
+ this.autoCollectAlways = opts.autoCollectAlways === true;
445
733
  const g = opts.autoGuardrailGroups ?? {};
446
734
  this.autoGuardrailGroups = {
447
735
  vitals: g.vitals ?? this.autoGuardrails,
@@ -450,6 +738,13 @@ var FlagsClientBrowser = class {
450
738
  };
451
739
  this.anonId = getOrCreateAnonId();
452
740
  this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
741
+ this.telemetry = new Telemetry({
742
+ endpoint: opts.telemetryUrl ?? DEFAULT_TELEMETRY_URL,
743
+ sdkKey: this.sdkKey,
744
+ side: "client",
745
+ env: this.env,
746
+ disabled: opts.disableTelemetry
747
+ });
453
748
  void this.buffer.flushPendingAlias();
454
749
  }
455
750
  async identify(user) {
@@ -479,10 +774,38 @@ var FlagsClientBrowser = class {
479
774
  const anyGroupOn = this.autoGuardrailGroups.vitals || this.autoGuardrailGroups.errors || this.autoGuardrailGroups.engagement;
480
775
  if (anyGroupOn && !this.guardrailsInstalled) {
481
776
  this.guardrailsInstalled = true;
482
- installAutoGuardrails(this.buffer, this.userId, this.anonId, this.autoGuardrailGroups);
777
+ installAutoGuardrails(
778
+ this.buffer,
779
+ this.userId,
780
+ this.anonId,
781
+ this.autoGuardrailGroups,
782
+ (problem, consequence, extras, kind) => this.reportError(problem, consequence, extras, kind),
783
+ [`${this.baseUrl}/`, DEFAULT_TELEMETRY_URL],
784
+ this.autoCollectAlways
785
+ );
483
786
  }
484
787
  this.notify();
485
788
  }
789
+ /**
790
+ * Report a structured error into the errors primitive. Flushes immediately
791
+ * (beacon-first) — error occurrences are near-real-time, never queued behind
792
+ * the 5s metric batch. Spam-guarded by a 30s dedup window + per-session cap.
793
+ */
794
+ reportError(problem, consequence, extras, kind) {
795
+ try {
796
+ const ev = buildSeeEvent(problem, consequence, extras, {
797
+ side: "client",
798
+ sdkVersion: version,
799
+ env: this.env,
800
+ url: typeof window !== "undefined" && window.location ? window.location.href : void 0,
801
+ userId: this.userId || void 0,
802
+ anonId: this.anonId
803
+ }, kind);
804
+ if (!this.seeLimiter.shouldSend(ev)) return;
805
+ this.buffer.sendNow([ev]);
806
+ } catch {
807
+ }
808
+ }
486
809
  get ready() {
487
810
  return this.evalResult !== null;
488
811
  }
@@ -499,12 +822,14 @@ var FlagsClientBrowser = class {
499
822
  this.evalResult = data;
500
823
  }
501
824
  getFlag(name) {
825
+ this.telemetry.emit("gate", name);
502
826
  if (this.evalResult === null) return false;
503
827
  const ov = readGateOverride(name);
504
828
  if (ov !== null) return ov;
505
829
  return this.evalResult.flags[name] ?? false;
506
830
  }
507
831
  getConfig(name, decode) {
832
+ this.telemetry.emit("config", name);
508
833
  if (this.evalResult === null) return void 0;
509
834
  const ov = readConfigOverride(name);
510
835
  const raw = ov !== void 0 ? ov : this.evalResult.configs?.[name];
@@ -518,6 +843,7 @@ var FlagsClientBrowser = class {
518
843
  }
519
844
  }
520
845
  getExperiment(name, defaultParams, decode, variants) {
846
+ this.telemetry.emit("experiment", name);
521
847
  const notIn = {
522
848
  inExperiment: false,
523
849
  group: "control",
@@ -582,6 +908,7 @@ var FlagsClientBrowser = class {
582
908
  * the per-switch state. Returns false for unknown killswitches / switches.
583
909
  */
584
910
  getKillswitch(name, switchKey) {
911
+ this.telemetry.emit("ks", name);
585
912
  if (this.evalResult === null) return false;
586
913
  const ks = this.evalResult.killswitches?.[name];
587
914
  if (ks === void 0) return false;
@@ -710,13 +1037,16 @@ var _client = null;
710
1037
  function shipeasy(opts) {
711
1038
  const ac = opts.autoCollect;
712
1039
  const blanket = ac === false ? false : true;
713
- const groups = ac && typeof ac === "object" ? ac : void 0;
1040
+ const acObj = ac && typeof ac === "object" ? ac : void 0;
1041
+ const groups = acObj ? { vitals: acObj.vitals, errors: acObj.errors, engagement: acObj.engagement } : void 0;
714
1042
  const baseUrl = opts.baseUrl ?? "https://cdn.shipeasy.ai";
715
1043
  const client = configureShipeasy({
716
1044
  sdkKey: opts.clientKey,
717
1045
  baseUrl,
718
1046
  autoGuardrails: blanket,
719
- autoGuardrailGroups: groups
1047
+ autoGuardrailGroups: groups,
1048
+ autoCollectAlways: acObj?.always === true,
1049
+ disableTelemetry: opts.disableTelemetry
720
1050
  });
721
1051
  injectI18nLoader(opts.clientKey, baseUrl, opts.i18nProfile);
722
1052
  flags.notifyMounted();
@@ -877,6 +1207,20 @@ var flags = {
877
1207
  return _client?.ready ?? false;
878
1208
  }
879
1209
  };
1210
+ function dispatchSee(problem, consequence, extras, kind) {
1211
+ if (!_client) {
1212
+ console.warn("[shipeasy] see() called before shipeasy({ clientKey }) \u2014 error dropped");
1213
+ return;
1214
+ }
1215
+ _client.reportError(problem, consequence, extras, kind);
1216
+ }
1217
+ var see = Object.assign(
1218
+ (problem) => startSeeChain(() => problem, dispatchSee),
1219
+ {
1220
+ Violation: (name) => startSeeViolationChain(name, dispatchSee),
1221
+ ControlFlowException: markExpected
1222
+ }
1223
+ );
880
1224
  var LABEL_MARKER_START = "\uFFF9";
881
1225
  var LABEL_MARKER_SEP = "\uFFFA";
882
1226
  var LABEL_MARKER_END = "\uFFFB";
@@ -1129,6 +1473,7 @@ var i18n = {
1129
1473
  readConfigOverride,
1130
1474
  readExpOverride,
1131
1475
  readGateOverride,
1476
+ see,
1132
1477
  shipeasy,
1133
1478
  version
1134
1479
  });