@shipeasy/sdk 3.1.0 → 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.
@@ -57,8 +57,208 @@ 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
+ 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
+
60
260
  // src/client/index.ts
61
- var version = "1.0.0";
261
+ var version = "4.0.0";
62
262
  var FLUSH_INTERVAL_MS = 5e3;
63
263
  var MAX_BUFFER = 100;
64
264
  var ANON_ID_KEY = "__se_anon_id";
@@ -92,6 +292,13 @@ var EventBuffer = class {
92
292
  this.timer = null;
93
293
  }
94
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
+ }
95
302
  pushExposure(experiment, group, userId, anonId) {
96
303
  const key = `${userId || anonId}:${experiment}`;
97
304
  if (this.exposureSeen.has(key)) return;
@@ -157,16 +364,29 @@ var EventBuffer = class {
157
364
  flush(useBeacon = false) {
158
365
  if (!this.queue.length) return;
159
366
  const batch = this.queue.splice(0);
160
- 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) {
161
378
  if (useBeacon && typeof navigator !== "undefined" && navigator.sendBeacon) {
162
379
  const beaconBody = JSON.stringify({ k: this.sdkKey, events: batch });
163
- navigator.sendBeacon(this.collectUrl, new Blob([beaconBody], { type: "text/plain" }));
164
- return;
380
+ try {
381
+ if (navigator.sendBeacon(this.collectUrl, new Blob([beaconBody], { type: "text/plain" })))
382
+ return;
383
+ } catch {
384
+ }
165
385
  }
166
386
  fetch(this.collectUrl, {
167
387
  method: "POST",
168
388
  headers: { "X-SDK-Key": this.sdkKey, "Content-Type": "application/json" },
169
- body,
389
+ body: JSON.stringify({ events: batch }),
170
390
  keepalive: true
171
391
  }).catch(() => {
172
392
  });
@@ -183,14 +403,12 @@ var EventBuffer = class {
183
403
  });
184
404
  }
185
405
  };
186
- var MAX_ERRORS_PER_SESSION = 5;
187
- function installAutoGuardrails(buffer, userId, anonId, groups) {
406
+ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignoreUrlPrefixes, always = false) {
188
407
  if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") return;
408
+ const shouldEmit = () => always || buffer.hasExposures();
189
409
  let lcp = null;
190
410
  let inp = null;
191
411
  let clsBad = false;
192
- let jsErrorCount = 0;
193
- let netErrorCount = 0;
194
412
  let navTimingFlushed = false;
195
413
  if (groups.vitals) {
196
414
  try {
@@ -229,68 +447,71 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
229
447
  if (groups.errors) {
230
448
  const origOnError = window.onerror;
231
449
  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
- });
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
+ );
241
461
  }
242
462
  if (typeof origOnError === "function") return origOnError(msg, source, lineno, _colno, err);
243
463
  return false;
244
464
  };
245
465
  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
- }
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
+ );
256
474
  });
257
475
  const origFetch = window.fetch;
258
476
  window.fetch = async function(...args) {
259
477
  const startedAt = typeof performance !== "undefined" ? performance.now() : 0;
260
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);
261
481
  let res;
262
482
  try {
263
483
  res = await origFetch.apply(this, args);
264
484
  } 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
- });
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
+ );
273
492
  }
274
493
  throw err;
275
494
  }
276
- if (res.status >= 500 && netErrorCount < MAX_ERRORS_PER_SESSION) {
277
- netErrorCount += 1;
495
+ if (!ignored && res.status >= 500) {
278
496
  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
- });
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
+ );
286
503
  }
287
504
  return res;
288
505
  };
289
506
  }
290
507
  const flushNavTiming = () => {
291
508
  if (navTimingFlushed) return;
509
+ if (!groups.vitals) {
510
+ navTimingFlushed = true;
511
+ return;
512
+ }
513
+ if (!shouldEmit()) return;
292
514
  navTimingFlushed = true;
293
- if (!groups.vitals) return;
294
515
  try {
295
516
  const navList = performance.getEntriesByType("navigation");
296
517
  const nav = navList[0];
@@ -325,7 +546,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
325
546
  };
326
547
  if (groups.engagement) {
327
548
  try {
328
- buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
549
+ if (shouldEmit()) buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
329
550
  } catch {
330
551
  }
331
552
  let lastEmit = Date.now();
@@ -333,6 +554,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
333
554
  document.addEventListener("visibilitychange", () => {
334
555
  if (document.visibilityState !== "visible") return;
335
556
  if (Date.now() - lastEmit < SESSION_GAP_MS) return;
557
+ if (!shouldEmit()) return;
336
558
  try {
337
559
  buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
338
560
  lastEmit = Date.now();
@@ -355,7 +577,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
355
577
  }
356
578
  const flushOnHide = () => {
357
579
  flushNavTiming();
358
- if (groups.vitals) {
580
+ if (groups.vitals && shouldEmit()) {
359
581
  if (lcp !== null) buffer.pushMetric("__auto_lcp", userId, anonId, { value: lcp });
360
582
  if (inp !== null) buffer.pushMetric("__auto_inp", userId, anonId, { value: inp });
361
583
  if (clsBad) buffer.pushMetric("__auto_cls_binary", userId, anonId, { value: 1 });
@@ -438,12 +660,14 @@ var FlagsClientBrowser = class {
438
660
  baseUrl;
439
661
  autoGuardrails;
440
662
  autoGuardrailGroups;
663
+ autoCollectAlways;
441
664
  env;
442
665
  evalResult = null;
443
666
  anonId;
444
667
  userId = "";
445
668
  buffer;
446
669
  telemetry;
670
+ seeLimiter = new SeeLimiter();
447
671
  guardrailsInstalled = false;
448
672
  listeners = /* @__PURE__ */ new Set();
449
673
  overrideListenerInstalled = false;
@@ -459,6 +683,7 @@ var FlagsClientBrowser = class {
459
683
  this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
460
684
  this.env = opts.env ?? "prod";
461
685
  this.autoGuardrails = opts.autoGuardrails !== false;
686
+ this.autoCollectAlways = opts.autoCollectAlways === true;
462
687
  const g = opts.autoGuardrailGroups ?? {};
463
688
  this.autoGuardrailGroups = {
464
689
  vitals: g.vitals ?? this.autoGuardrails,
@@ -503,10 +728,38 @@ var FlagsClientBrowser = class {
503
728
  const anyGroupOn = this.autoGuardrailGroups.vitals || this.autoGuardrailGroups.errors || this.autoGuardrailGroups.engagement;
504
729
  if (anyGroupOn && !this.guardrailsInstalled) {
505
730
  this.guardrailsInstalled = true;
506
- 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
+ );
507
740
  }
508
741
  this.notify();
509
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
+ }
510
763
  get ready() {
511
764
  return this.evalResult !== null;
512
765
  }
@@ -738,13 +991,15 @@ var _client = null;
738
991
  function shipeasy(opts) {
739
992
  const ac = opts.autoCollect;
740
993
  const blanket = ac === false ? false : true;
741
- 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;
742
996
  const baseUrl = opts.baseUrl ?? "https://cdn.shipeasy.ai";
743
997
  const client = configureShipeasy({
744
998
  sdkKey: opts.clientKey,
745
999
  baseUrl,
746
1000
  autoGuardrails: blanket,
747
1001
  autoGuardrailGroups: groups,
1002
+ autoCollectAlways: acObj?.always === true,
748
1003
  disableTelemetry: opts.disableTelemetry
749
1004
  });
750
1005
  injectI18nLoader(opts.clientKey, baseUrl, opts.i18nProfile);
@@ -906,6 +1161,20 @@ var flags = {
906
1161
  return _client?.ready ?? false;
907
1162
  }
908
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
+ );
909
1178
  var LABEL_MARKER_START = "\uFFF9";
910
1179
  var LABEL_MARKER_SEP = "\uFFFA";
911
1180
  var LABEL_MARKER_END = "\uFFFB";
@@ -1157,6 +1426,7 @@ export {
1157
1426
  readConfigOverride,
1158
1427
  readExpOverride,
1159
1428
  readGateOverride,
1429
+ see,
1160
1430
  shipeasy,
1161
1431
  version
1162
1432
  };
@@ -1,4 +1,63 @@
1
- declare const version = "1.0.0";
1
+ type SeeExtras = Record<string, string | number | boolean | null | undefined>;
2
+ type SeeKind = "caught" | "uncaught" | "unhandled_rejection" | "network" | "violation";
3
+ /** Built by `causesThe(subject).to(outcome)` — never constructed by hand. */
4
+ interface Consequence {
5
+ readonly __seConsequence: true;
6
+ readonly subject: string;
7
+ readonly outcome: string;
8
+ }
9
+ /**
10
+ * Non-exception problem, built by `violation(name)`. A plain branded object
11
+ * (not an Error subclass) so `.message()` can be a builder method without
12
+ * colliding with `Error.prototype.message`.
13
+ */
14
+ interface Violation {
15
+ readonly __seViolation: true;
16
+ readonly violationName: string;
17
+ readonly violationMessage?: string;
18
+ /** Attach free-form detail. Variable data goes HERE (or in extras), never in the name. */
19
+ message(msg: string): Violation;
20
+ }
21
+ /** Wire shape — the `type:"error"` RawEvent variant accepted by POST /collect. */
22
+ interface SeeErrorEvent {
23
+ type: "error";
24
+ kind: SeeKind;
25
+ /** Error class/name (e.g. "TypeError") or the violation name. */
26
+ error_type: string;
27
+ message: string;
28
+ stack?: string;
29
+ /** Consequence: "<error_type> causes the <subject> to <outcome>". */
30
+ subject: string;
31
+ outcome: string;
32
+ extras?: Record<string, string | number | boolean>;
33
+ url?: string;
34
+ user_id?: string;
35
+ anonymous_id?: string;
36
+ side: "client" | "server";
37
+ env?: string;
38
+ sdk_version: string;
39
+ ts: number;
40
+ }
41
+ interface SeeExtrasTail {
42
+ /** Attach debugging metadata. Callable repeatedly — keys merge, later wins. */
43
+ extras(extras: SeeExtras): SeeExtrasTail;
44
+ }
45
+ interface SeeOutcomeStep {
46
+ /** The user-visible impact: `.causes_the("checkout").to("use cached prices")`. */
47
+ to(outcome: string): SeeExtrasTail;
48
+ }
49
+ interface SeeChain {
50
+ /** Start the consequence sentence — the product surface affected. */
51
+ causes_the(subject: string): SeeOutcomeStep;
52
+ /** camelCase alias of {@link SeeChain.causes_the}. */
53
+ causesThe(subject: string): SeeOutcomeStep;
54
+ }
55
+ interface SeeViolationChain extends SeeChain {
56
+ /** Free-form detail. Variable data goes here (or extras), never in the name. */
57
+ message(msg: string): SeeViolationChain;
58
+ }
59
+
60
+ declare const version = "4.0.0";
2
61
  interface User {
3
62
  user_id?: string;
4
63
  anonymous_id?: string;
@@ -68,6 +127,7 @@ declare class FlagsClient {
68
127
  private readonly baseUrl;
69
128
  private readonly env;
70
129
  private readonly telemetry;
130
+ private readonly seeLimiter;
71
131
  private flagsBlob;
72
132
  private expsBlob;
73
133
  private flagsEtag;
@@ -87,6 +147,12 @@ declare class FlagsClient {
87
147
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
88
148
  getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
89
149
  track(userId: string, eventName: string, props?: Record<string, unknown>): void;
150
+ /**
151
+ * Report a structured error into the errors primitive. Fire-and-forget —
152
+ * never blocks or throws into the request path. Spam-guarded by a 30s
153
+ * dedup window + per-process cap.
154
+ */
155
+ reportError(problem: unknown, consequence: Consequence, extras?: SeeExtras, kind?: SeeKind): void;
90
156
  /**
91
157
  * Evaluate all flags, configs, and experiments for a user against the locally
92
158
  * cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
@@ -229,5 +295,49 @@ declare const flags: {
229
295
  */
230
296
  evaluate(user: User, rawUrl?: string): BootstrapPayload;
231
297
  };
298
+ interface SeeApi {
299
+ /**
300
+ * Report a handled problem and its product consequence:
301
+ *
302
+ * ```ts
303
+ * import { see } from "@shipeasy/sdk/server";
304
+ *
305
+ * try {
306
+ * await chargeCard(order);
307
+ * } catch (e) {
308
+ * see(e).causes_the("payment").to("use the backup processor").extras({ order_id: order.id });
309
+ * await chargeViaBackup(order);
310
+ * }
311
+ * ```
312
+ *
313
+ * The chain dispatches on the next microtask — fire-and-forget into the
314
+ * errors primitive (grouped by fingerprint, near-real-time timeseries).
315
+ * If you don't know the consequence of an exception, don't catch it.
316
+ */
317
+ (problem: unknown): SeeChain;
318
+ /**
319
+ * Report a non-exception problem. Prefer passing a caught Error to `see()`
320
+ * when one exists. The name is a stable identifier (it participates in the
321
+ * issue fingerprint) — variable data goes in `.message()` or `.extras()`.
322
+ *
323
+ * ```ts
324
+ * if (results.length > LIMIT) {
325
+ * see.Violation("large query").causes_the("search results").to("be trimmed");
326
+ * }
327
+ * ```
328
+ */
329
+ Violation(name: string): SeeViolationChain;
330
+ /**
331
+ * Mark an exception as expected control flow — documents the expectation and
332
+ * reports nothing. The reason must start with "because".
333
+ */
334
+ ControlFlowException(err: unknown, because: string): void;
335
+ }
336
+ /**
337
+ * Structured error reporter — the whole grammar hangs off this one import.
338
+ * Safe to import anywhere; a call before `shipeasy({ serverKey })` warns and
339
+ * drops (never throws).
340
+ */
341
+ declare const see: SeeApi;
232
342
 
233
- export { type BootstrapHtmlOptions, type BootstrapPayload, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type I18nForRequest, type LabelFile, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, shipeasy, version };
343
+ export { type BootstrapHtmlOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type I18nForRequest, type LabelFile, type SeeApi, type SeeChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, type Violation, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, see, shipeasy, version };