@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.
@@ -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 };
@@ -38,6 +38,7 @@ __export(server_exports, {
38
38
  getBootstrapHtml: () => getBootstrapHtml,
39
39
  getShipeasyServerClient: () => getShipeasyServerClient,
40
40
  i18n: () => i18n,
41
+ see: () => see,
41
42
  shipeasy: () => shipeasy,
42
43
  version: () => version
43
44
  });
@@ -103,8 +104,204 @@ function send(url) {
103
104
  }
104
105
  }
105
106
 
107
+ // src/see/core.ts
108
+ var SEE_MAX_MESSAGE = 500;
109
+ var SEE_MAX_STACK = 8e3;
110
+ var SEE_MAX_SUBJECT = 200;
111
+ var SEE_MAX_EXTRA_VALUE = 200;
112
+ var SEE_MAX_EXTRA_KEYS = 20;
113
+ var SEE_DEDUP_WINDOW_MS = 3e4;
114
+ var SEE_MAX_PER_SESSION = 25;
115
+ function causesThe(subject) {
116
+ return {
117
+ to(outcome) {
118
+ return {
119
+ __seConsequence: true,
120
+ subject: truncate(String(subject), SEE_MAX_SUBJECT),
121
+ outcome: truncate(String(outcome), SEE_MAX_SUBJECT)
122
+ };
123
+ }
124
+ };
125
+ }
126
+ function violation(name) {
127
+ const make = (msg) => ({
128
+ __seViolation: true,
129
+ violationName: String(name),
130
+ ...msg !== void 0 ? { violationMessage: msg } : {},
131
+ message(m) {
132
+ return make(String(m));
133
+ }
134
+ });
135
+ return make();
136
+ }
137
+ function isViolation(p) {
138
+ return typeof p === "object" && p !== null && p.__seViolation === true;
139
+ }
140
+ var EXPECTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-expected");
141
+ function markExpected(err, because) {
142
+ if (typeof err !== "object" || err === null) return;
143
+ try {
144
+ Object.defineProperty(err, EXPECTED_SYM, {
145
+ value: String(because),
146
+ enumerable: false,
147
+ configurable: true
148
+ });
149
+ } catch {
150
+ }
151
+ }
152
+ function truncate(s, max) {
153
+ return s.length > max ? s.slice(0, max) : s;
154
+ }
155
+ function sanitizeExtras(extras) {
156
+ if (!extras || typeof extras !== "object") return void 0;
157
+ const out = {};
158
+ let n = 0;
159
+ for (const [k, v] of Object.entries(extras)) {
160
+ if (v === null || v === void 0) continue;
161
+ if (n >= SEE_MAX_EXTRA_KEYS) break;
162
+ if (typeof v === "string") out[k] = truncate(v, SEE_MAX_EXTRA_VALUE);
163
+ else if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
164
+ else if (typeof v === "boolean") out[k] = v;
165
+ else continue;
166
+ n += 1;
167
+ }
168
+ return n > 0 ? out : void 0;
169
+ }
170
+ function captureCallsiteStack() {
171
+ const raw = new Error().stack;
172
+ if (!raw) return void 0;
173
+ const lines = raw.split("\n");
174
+ const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
175
+ return kept.length ? kept.join("\n") : void 0;
176
+ }
177
+ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
178
+ let errorType;
179
+ let message;
180
+ let stack;
181
+ let kind;
182
+ if (isViolation(problem)) {
183
+ errorType = problem.violationName;
184
+ message = problem.violationMessage ?? problem.violationName;
185
+ stack = captureCallsiteStack();
186
+ kind = kindOverride ?? "violation";
187
+ } else if (problem instanceof Error) {
188
+ errorType = problem.name || "Error";
189
+ message = problem.message || String(problem);
190
+ stack = problem.stack ?? void 0;
191
+ kind = kindOverride ?? "caught";
192
+ } else {
193
+ errorType = "Error";
194
+ message = typeof problem === "string" ? problem : safeString(problem);
195
+ stack = captureCallsiteStack();
196
+ kind = kindOverride ?? "caught";
197
+ }
198
+ const ev = {
199
+ type: "error",
200
+ kind,
201
+ error_type: truncate(errorType, SEE_MAX_SUBJECT),
202
+ message: truncate(message, SEE_MAX_MESSAGE),
203
+ subject: consequence.subject,
204
+ outcome: consequence.outcome,
205
+ side: ctx.side,
206
+ sdk_version: ctx.sdkVersion,
207
+ ts: Date.now()
208
+ };
209
+ if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
210
+ const cleanExtras = sanitizeExtras(extras);
211
+ if (cleanExtras) ev.extras = cleanExtras;
212
+ if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
213
+ if (ctx.userId) ev.user_id = ctx.userId;
214
+ if (ctx.anonId) ev.anonymous_id = ctx.anonId;
215
+ if (ctx.env) ev.env = ctx.env;
216
+ return ev;
217
+ }
218
+ function safeString(v) {
219
+ try {
220
+ return typeof v === "object" ? JSON.stringify(v) : String(v);
221
+ } catch {
222
+ return String(v);
223
+ }
224
+ }
225
+ var scheduleMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : (cb) => {
226
+ void Promise.resolve().then(cb);
227
+ };
228
+ function startSeeChain(getProblem, dispatch) {
229
+ let subject;
230
+ let outcome;
231
+ let collected;
232
+ let flushed = false;
233
+ scheduleMicrotask(() => {
234
+ if (flushed) return;
235
+ flushed = true;
236
+ dispatch(
237
+ getProblem(),
238
+ causesThe(subject ?? "the 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
+
106
303
  // src/server/index.ts
107
- var version = "1.0.0";
304
+ var version = "4.0.0";
108
305
  var C1 = 3432918353;
109
306
  var C2 = 461845907;
110
307
  function murmur3(key) {
@@ -254,6 +451,7 @@ var FlagsClient = class {
254
451
  baseUrl;
255
452
  env;
256
453
  telemetry;
454
+ seeLimiter = new SeeLimiter();
257
455
  flagsBlob = null;
258
456
  expsBlob = null;
259
457
  flagsEtag = null;
@@ -406,6 +604,27 @@ var FlagsClient = class {
406
604
  body
407
605
  }).catch((err) => console.warn("[shipeasy] track failed:", String(err)));
408
606
  }
607
+ /**
608
+ * Report a structured error into the errors primitive. Fire-and-forget —
609
+ * never blocks or throws into the request path. Spam-guarded by a 30s
610
+ * dedup window + per-process cap.
611
+ */
612
+ reportError(problem, consequence, extras, kind) {
613
+ try {
614
+ const ev = buildSeeEvent(problem, consequence, extras, {
615
+ side: "server",
616
+ sdkVersion: version,
617
+ env: this.env
618
+ }, kind);
619
+ if (!this.seeLimiter.shouldSend(ev)) return;
620
+ globalThis.fetch(`${this.baseUrl}/collect`, {
621
+ method: "POST",
622
+ headers: { "X-SDK-Key": this.apiKey, "Content-Type": "text/plain" },
623
+ body: JSON.stringify({ events: [ev] })
624
+ }).catch((err) => console.warn("[shipeasy] see() send failed:", String(err)));
625
+ } catch {
626
+ }
627
+ }
409
628
  /**
410
629
  * Evaluate all flags, configs, and experiments for a user against the locally
411
630
  * cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
@@ -714,6 +933,20 @@ var flags = {
714
933
  };
715
934
  }
716
935
  };
936
+ function dispatchSee(problem, consequence, extras, kind) {
937
+ if (!_server) {
938
+ console.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
939
+ return;
940
+ }
941
+ _server.reportError(problem, consequence, extras, kind);
942
+ }
943
+ var see = Object.assign(
944
+ (problem) => startSeeChain(() => problem, dispatchSee),
945
+ {
946
+ Violation: (name) => startSeeViolationChain(name, dispatchSee),
947
+ ControlFlowException: markExpected
948
+ }
949
+ );
717
950
  // Annotate the CommonJS export names for ESM import in node:
718
951
  0 && (module.exports = {
719
952
  FlagsClient,
@@ -724,6 +957,7 @@ var flags = {
724
957
  getBootstrapHtml,
725
958
  getShipeasyServerClient,
726
959
  i18n,
960
+ see,
727
961
  shipeasy,
728
962
  version
729
963
  });
@@ -60,8 +60,204 @@ function send(url) {
60
60
  }
61
61
  }
62
62
 
63
+ // src/see/core.ts
64
+ var SEE_MAX_MESSAGE = 500;
65
+ var SEE_MAX_STACK = 8e3;
66
+ var SEE_MAX_SUBJECT = 200;
67
+ var SEE_MAX_EXTRA_VALUE = 200;
68
+ var SEE_MAX_EXTRA_KEYS = 20;
69
+ var SEE_DEDUP_WINDOW_MS = 3e4;
70
+ var SEE_MAX_PER_SESSION = 25;
71
+ function causesThe(subject) {
72
+ return {
73
+ to(outcome) {
74
+ return {
75
+ __seConsequence: true,
76
+ subject: truncate(String(subject), SEE_MAX_SUBJECT),
77
+ outcome: truncate(String(outcome), SEE_MAX_SUBJECT)
78
+ };
79
+ }
80
+ };
81
+ }
82
+ function violation(name) {
83
+ const make = (msg) => ({
84
+ __seViolation: true,
85
+ violationName: String(name),
86
+ ...msg !== void 0 ? { violationMessage: msg } : {},
87
+ message(m) {
88
+ return make(String(m));
89
+ }
90
+ });
91
+ return make();
92
+ }
93
+ function isViolation(p) {
94
+ return typeof p === "object" && p !== null && p.__seViolation === true;
95
+ }
96
+ var EXPECTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-expected");
97
+ function markExpected(err, because) {
98
+ if (typeof err !== "object" || err === null) return;
99
+ try {
100
+ Object.defineProperty(err, EXPECTED_SYM, {
101
+ value: String(because),
102
+ enumerable: false,
103
+ configurable: true
104
+ });
105
+ } catch {
106
+ }
107
+ }
108
+ function truncate(s, max) {
109
+ return s.length > max ? s.slice(0, max) : s;
110
+ }
111
+ function sanitizeExtras(extras) {
112
+ if (!extras || typeof extras !== "object") return void 0;
113
+ const out = {};
114
+ let n = 0;
115
+ for (const [k, v] of Object.entries(extras)) {
116
+ if (v === null || v === void 0) continue;
117
+ if (n >= SEE_MAX_EXTRA_KEYS) break;
118
+ if (typeof v === "string") out[k] = truncate(v, SEE_MAX_EXTRA_VALUE);
119
+ else if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
120
+ else if (typeof v === "boolean") out[k] = v;
121
+ else continue;
122
+ n += 1;
123
+ }
124
+ return n > 0 ? out : void 0;
125
+ }
126
+ function captureCallsiteStack() {
127
+ const raw = new Error().stack;
128
+ if (!raw) return void 0;
129
+ const lines = raw.split("\n");
130
+ const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
131
+ return kept.length ? kept.join("\n") : void 0;
132
+ }
133
+ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
134
+ let errorType;
135
+ let message;
136
+ let stack;
137
+ let kind;
138
+ if (isViolation(problem)) {
139
+ errorType = problem.violationName;
140
+ message = problem.violationMessage ?? problem.violationName;
141
+ stack = captureCallsiteStack();
142
+ kind = kindOverride ?? "violation";
143
+ } else if (problem instanceof Error) {
144
+ errorType = problem.name || "Error";
145
+ message = problem.message || String(problem);
146
+ stack = problem.stack ?? void 0;
147
+ kind = kindOverride ?? "caught";
148
+ } else {
149
+ errorType = "Error";
150
+ message = typeof problem === "string" ? problem : safeString(problem);
151
+ stack = captureCallsiteStack();
152
+ kind = kindOverride ?? "caught";
153
+ }
154
+ const ev = {
155
+ type: "error",
156
+ kind,
157
+ error_type: truncate(errorType, SEE_MAX_SUBJECT),
158
+ message: truncate(message, SEE_MAX_MESSAGE),
159
+ subject: consequence.subject,
160
+ outcome: consequence.outcome,
161
+ side: ctx.side,
162
+ sdk_version: ctx.sdkVersion,
163
+ ts: Date.now()
164
+ };
165
+ if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
166
+ const cleanExtras = sanitizeExtras(extras);
167
+ if (cleanExtras) ev.extras = cleanExtras;
168
+ if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
169
+ if (ctx.userId) ev.user_id = ctx.userId;
170
+ if (ctx.anonId) ev.anonymous_id = ctx.anonId;
171
+ if (ctx.env) ev.env = ctx.env;
172
+ return ev;
173
+ }
174
+ function safeString(v) {
175
+ try {
176
+ return typeof v === "object" ? JSON.stringify(v) : String(v);
177
+ } catch {
178
+ return String(v);
179
+ }
180
+ }
181
+ var scheduleMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : (cb) => {
182
+ void Promise.resolve().then(cb);
183
+ };
184
+ function startSeeChain(getProblem, dispatch) {
185
+ let subject;
186
+ let outcome;
187
+ let collected;
188
+ let flushed = false;
189
+ scheduleMicrotask(() => {
190
+ if (flushed) return;
191
+ flushed = true;
192
+ dispatch(
193
+ getProblem(),
194
+ causesThe(subject ?? "the app").to(outcome ?? "hit an error"),
195
+ collected
196
+ );
197
+ });
198
+ const tail = {
199
+ extras(x) {
200
+ if (x && typeof x === "object") collected = { ...collected, ...x };
201
+ return tail;
202
+ }
203
+ };
204
+ const step = {
205
+ to(o) {
206
+ outcome = String(o);
207
+ return tail;
208
+ }
209
+ };
210
+ const start = (s) => {
211
+ subject = String(s);
212
+ return step;
213
+ };
214
+ return { causes_the: start, causesThe: start };
215
+ }
216
+ function startSeeViolationChain(name, dispatch) {
217
+ let msg;
218
+ const base = startSeeChain(
219
+ () => msg !== void 0 ? violation(name).message(msg) : violation(name),
220
+ dispatch
221
+ );
222
+ const chain = {
223
+ ...base,
224
+ message(m) {
225
+ msg = String(m);
226
+ return chain;
227
+ }
228
+ };
229
+ return chain;
230
+ }
231
+ function topStackLine(stack) {
232
+ if (!stack) return "";
233
+ for (const line of stack.split("\n")) {
234
+ if (/^\s*at |@|:\d+:\d+/.test(line)) return line.trim().slice(0, 200);
235
+ }
236
+ return "";
237
+ }
238
+ var SeeLimiter = class {
239
+ constructor(maxPerSession = SEE_MAX_PER_SESSION, dedupWindowMs = SEE_DEDUP_WINDOW_MS) {
240
+ this.maxPerSession = maxPerSession;
241
+ this.dedupWindowMs = dedupWindowMs;
242
+ }
243
+ maxPerSession;
244
+ dedupWindowMs;
245
+ lastSent = /* @__PURE__ */ new Map();
246
+ sent = 0;
247
+ shouldSend(ev) {
248
+ if (this.sent >= this.maxPerSession) return false;
249
+ const key = `${ev.kind}|${ev.error_type}|${ev.message.slice(0, 200)}|${topStackLine(ev.stack)}`;
250
+ const now = Date.now();
251
+ const prev = this.lastSent.get(key);
252
+ if (prev !== void 0 && now - prev < this.dedupWindowMs) return false;
253
+ this.lastSent.set(key, now);
254
+ this.sent += 1;
255
+ return true;
256
+ }
257
+ };
258
+
63
259
  // src/server/index.ts
64
- var version = "1.0.0";
260
+ var version = "4.0.0";
65
261
  var C1 = 3432918353;
66
262
  var C2 = 461845907;
67
263
  function murmur3(key) {
@@ -211,6 +407,7 @@ var FlagsClient = class {
211
407
  baseUrl;
212
408
  env;
213
409
  telemetry;
410
+ seeLimiter = new SeeLimiter();
214
411
  flagsBlob = null;
215
412
  expsBlob = null;
216
413
  flagsEtag = null;
@@ -363,6 +560,27 @@ var FlagsClient = class {
363
560
  body
364
561
  }).catch((err) => console.warn("[shipeasy] track failed:", String(err)));
365
562
  }
563
+ /**
564
+ * Report a structured error into the errors primitive. Fire-and-forget —
565
+ * never blocks or throws into the request path. Spam-guarded by a 30s
566
+ * dedup window + per-process cap.
567
+ */
568
+ reportError(problem, consequence, extras, kind) {
569
+ try {
570
+ const ev = buildSeeEvent(problem, consequence, extras, {
571
+ side: "server",
572
+ sdkVersion: version,
573
+ env: this.env
574
+ }, kind);
575
+ if (!this.seeLimiter.shouldSend(ev)) return;
576
+ globalThis.fetch(`${this.baseUrl}/collect`, {
577
+ method: "POST",
578
+ headers: { "X-SDK-Key": this.apiKey, "Content-Type": "text/plain" },
579
+ body: JSON.stringify({ events: [ev] })
580
+ }).catch((err) => console.warn("[shipeasy] see() send failed:", String(err)));
581
+ } catch {
582
+ }
583
+ }
366
584
  /**
367
585
  * Evaluate all flags, configs, and experiments for a user against the locally
368
586
  * cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
@@ -671,6 +889,20 @@ var flags = {
671
889
  };
672
890
  }
673
891
  };
892
+ function dispatchSee(problem, consequence, extras, kind) {
893
+ if (!_server) {
894
+ console.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
895
+ return;
896
+ }
897
+ _server.reportError(problem, consequence, extras, kind);
898
+ }
899
+ var see = Object.assign(
900
+ (problem) => startSeeChain(() => problem, dispatchSee),
901
+ {
902
+ Violation: (name) => startSeeViolationChain(name, dispatchSee),
903
+ ControlFlowException: markExpected
904
+ }
905
+ );
674
906
  export {
675
907
  FlagsClient,
676
908
  _resetShipeasyServerForTests,
@@ -680,6 +912,7 @@ export {
680
912
  getBootstrapHtml,
681
913
  getShipeasyServerClient,
682
914
  i18n,
915
+ see,
683
916
  shipeasy,
684
917
  version
685
918
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipeasy/sdk",
3
- "version": "3.1.0",
3
+ "version": "4.1.0",
4
4
  "description": "Shipeasy SDK — feature gates, runtime configs, experiments, and metrics for the Shipeasy hosted service.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://shipeasy.ai",