@shipeasy/sdk 3.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,85 @@
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
+ /**
22
+ * Identity of a problem that see() already reported, carried on the wire as
23
+ * the `caused_by` of a later occurrence. Holds exactly the fields the worker's
24
+ * fingerprint function consumes (raw `message`/`stack` — the server normalizes
25
+ * them) so the backend can recompute the prior issue's fingerprint and link
26
+ * the two issues. See `findCausedBy` for how the link is discovered.
27
+ */
28
+ interface SeeCausedBy {
29
+ error_type: string;
30
+ message: string;
31
+ stack?: string;
32
+ subject: string;
33
+ outcome: string;
34
+ }
35
+ /** Wire shape — the `type:"error"` RawEvent variant accepted by POST /collect. */
36
+ interface SeeErrorEvent {
37
+ type: "error";
38
+ kind: SeeKind;
39
+ /** Error class/name (e.g. "TypeError") or the violation name. */
40
+ error_type: string;
41
+ message: string;
42
+ stack?: string;
43
+ /** Consequence: "<error_type> causes the <subject> to <outcome>". */
44
+ subject: string;
45
+ outcome: string;
46
+ extras?: Record<string, string | number | boolean>;
47
+ url?: string;
48
+ user_id?: string;
49
+ anonymous_id?: string;
50
+ side: "client" | "server";
51
+ env?: string;
52
+ sdk_version: string;
53
+ ts: number;
54
+ /**
55
+ * The earlier reported problem this occurrence descends from — present when
56
+ * the same error was caught + reported at an inner boundary and then
57
+ * re-thrown (or wrapped via `{ cause }`) and reported again at an outer one.
58
+ * Lets the backend stitch the two issues into a cause chain instead of
59
+ * double-counting them as unrelated.
60
+ */
61
+ caused_by?: SeeCausedBy;
62
+ }
63
+ interface SeeExtrasTail {
64
+ /** Attach debugging metadata. Callable repeatedly — keys merge, later wins. */
65
+ extras(extras: SeeExtras): SeeExtrasTail;
66
+ }
67
+ interface SeeOutcomeStep {
68
+ /** The user-visible impact: `.causes_the("checkout").to("use cached prices")`. */
69
+ to(outcome: string): SeeExtrasTail;
70
+ }
71
+ interface SeeChain {
72
+ /** Start the consequence sentence — the product surface affected. */
73
+ causes_the(subject: string): SeeOutcomeStep;
74
+ /** camelCase alias of {@link SeeChain.causes_the}. */
75
+ causesThe(subject: string): SeeOutcomeStep;
76
+ }
77
+ interface SeeViolationChain extends SeeChain {
78
+ /** Free-form detail. Variable data goes here (or extras), never in the name. */
79
+ message(msg: string): SeeViolationChain;
80
+ }
81
+
82
+ declare const version = "4.0.0";
2
83
  interface User {
3
84
  user_id?: string;
4
85
  anonymous_id?: string;
@@ -68,6 +149,7 @@ declare class FlagsClient {
68
149
  private readonly baseUrl;
69
150
  private readonly env;
70
151
  private readonly telemetry;
152
+ private readonly seeLimiter;
71
153
  private flagsBlob;
72
154
  private expsBlob;
73
155
  private flagsEtag;
@@ -87,6 +169,12 @@ declare class FlagsClient {
87
169
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
88
170
  getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
89
171
  track(userId: string, eventName: string, props?: Record<string, unknown>): void;
172
+ /**
173
+ * Report a structured error into the errors primitive. Fire-and-forget —
174
+ * never blocks or throws into the request path. Spam-guarded by a 30s
175
+ * dedup window + per-process cap.
176
+ */
177
+ reportError(problem: unknown, consequence: Consequence, extras?: SeeExtras, kind?: SeeKind): void;
90
178
  /**
91
179
  * Evaluate all flags, configs, and experiments for a user against the locally
92
180
  * cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
@@ -229,5 +317,49 @@ declare const flags: {
229
317
  */
230
318
  evaluate(user: User, rawUrl?: string): BootstrapPayload;
231
319
  };
320
+ interface SeeApi {
321
+ /**
322
+ * Report a handled problem and its product consequence:
323
+ *
324
+ * ```ts
325
+ * import { see } from "@shipeasy/sdk/server";
326
+ *
327
+ * try {
328
+ * await chargeCard(order);
329
+ * } catch (e) {
330
+ * see(e).causes_the("payment").to("use the backup processor").extras({ order_id: order.id });
331
+ * await chargeViaBackup(order);
332
+ * }
333
+ * ```
334
+ *
335
+ * The chain dispatches on the next microtask — fire-and-forget into the
336
+ * errors primitive (grouped by fingerprint, near-real-time timeseries).
337
+ * If you don't know the consequence of an exception, don't catch it.
338
+ */
339
+ (problem: unknown): SeeChain;
340
+ /**
341
+ * Report a non-exception problem. Prefer passing a caught Error to `see()`
342
+ * when one exists. The name is a stable identifier (it participates in the
343
+ * issue fingerprint) — variable data goes in `.message()` or `.extras()`.
344
+ *
345
+ * ```ts
346
+ * if (results.length > LIMIT) {
347
+ * see.Violation("large query").causes_the("search results").to("be trimmed");
348
+ * }
349
+ * ```
350
+ */
351
+ Violation(name: string): SeeViolationChain;
352
+ /**
353
+ * Mark an exception as expected control flow — documents the expectation and
354
+ * reports nothing. The reason must start with "because".
355
+ */
356
+ ControlFlowException(err: unknown, because: string): void;
357
+ }
358
+ /**
359
+ * Structured error reporter — the whole grammar hangs off this one import.
360
+ * Safe to import anywhere; a call before `shipeasy({ serverKey })` warns and
361
+ * drops (never throws).
362
+ */
363
+ declare const see: SeeApi;
232
364
 
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 };
365
+ 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 };
@@ -1,4 +1,85 @@
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
+ /**
22
+ * Identity of a problem that see() already reported, carried on the wire as
23
+ * the `caused_by` of a later occurrence. Holds exactly the fields the worker's
24
+ * fingerprint function consumes (raw `message`/`stack` — the server normalizes
25
+ * them) so the backend can recompute the prior issue's fingerprint and link
26
+ * the two issues. See `findCausedBy` for how the link is discovered.
27
+ */
28
+ interface SeeCausedBy {
29
+ error_type: string;
30
+ message: string;
31
+ stack?: string;
32
+ subject: string;
33
+ outcome: string;
34
+ }
35
+ /** Wire shape — the `type:"error"` RawEvent variant accepted by POST /collect. */
36
+ interface SeeErrorEvent {
37
+ type: "error";
38
+ kind: SeeKind;
39
+ /** Error class/name (e.g. "TypeError") or the violation name. */
40
+ error_type: string;
41
+ message: string;
42
+ stack?: string;
43
+ /** Consequence: "<error_type> causes the <subject> to <outcome>". */
44
+ subject: string;
45
+ outcome: string;
46
+ extras?: Record<string, string | number | boolean>;
47
+ url?: string;
48
+ user_id?: string;
49
+ anonymous_id?: string;
50
+ side: "client" | "server";
51
+ env?: string;
52
+ sdk_version: string;
53
+ ts: number;
54
+ /**
55
+ * The earlier reported problem this occurrence descends from — present when
56
+ * the same error was caught + reported at an inner boundary and then
57
+ * re-thrown (or wrapped via `{ cause }`) and reported again at an outer one.
58
+ * Lets the backend stitch the two issues into a cause chain instead of
59
+ * double-counting them as unrelated.
60
+ */
61
+ caused_by?: SeeCausedBy;
62
+ }
63
+ interface SeeExtrasTail {
64
+ /** Attach debugging metadata. Callable repeatedly — keys merge, later wins. */
65
+ extras(extras: SeeExtras): SeeExtrasTail;
66
+ }
67
+ interface SeeOutcomeStep {
68
+ /** The user-visible impact: `.causes_the("checkout").to("use cached prices")`. */
69
+ to(outcome: string): SeeExtrasTail;
70
+ }
71
+ interface SeeChain {
72
+ /** Start the consequence sentence — the product surface affected. */
73
+ causes_the(subject: string): SeeOutcomeStep;
74
+ /** camelCase alias of {@link SeeChain.causes_the}. */
75
+ causesThe(subject: string): SeeOutcomeStep;
76
+ }
77
+ interface SeeViolationChain extends SeeChain {
78
+ /** Free-form detail. Variable data goes here (or extras), never in the name. */
79
+ message(msg: string): SeeViolationChain;
80
+ }
81
+
82
+ declare const version = "4.0.0";
2
83
  interface User {
3
84
  user_id?: string;
4
85
  anonymous_id?: string;
@@ -68,6 +149,7 @@ declare class FlagsClient {
68
149
  private readonly baseUrl;
69
150
  private readonly env;
70
151
  private readonly telemetry;
152
+ private readonly seeLimiter;
71
153
  private flagsBlob;
72
154
  private expsBlob;
73
155
  private flagsEtag;
@@ -87,6 +169,12 @@ declare class FlagsClient {
87
169
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
88
170
  getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
89
171
  track(userId: string, eventName: string, props?: Record<string, unknown>): void;
172
+ /**
173
+ * Report a structured error into the errors primitive. Fire-and-forget —
174
+ * never blocks or throws into the request path. Spam-guarded by a 30s
175
+ * dedup window + per-process cap.
176
+ */
177
+ reportError(problem: unknown, consequence: Consequence, extras?: SeeExtras, kind?: SeeKind): void;
90
178
  /**
91
179
  * Evaluate all flags, configs, and experiments for a user against the locally
92
180
  * cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
@@ -229,5 +317,49 @@ declare const flags: {
229
317
  */
230
318
  evaluate(user: User, rawUrl?: string): BootstrapPayload;
231
319
  };
320
+ interface SeeApi {
321
+ /**
322
+ * Report a handled problem and its product consequence:
323
+ *
324
+ * ```ts
325
+ * import { see } from "@shipeasy/sdk/server";
326
+ *
327
+ * try {
328
+ * await chargeCard(order);
329
+ * } catch (e) {
330
+ * see(e).causes_the("payment").to("use the backup processor").extras({ order_id: order.id });
331
+ * await chargeViaBackup(order);
332
+ * }
333
+ * ```
334
+ *
335
+ * The chain dispatches on the next microtask — fire-and-forget into the
336
+ * errors primitive (grouped by fingerprint, near-real-time timeseries).
337
+ * If you don't know the consequence of an exception, don't catch it.
338
+ */
339
+ (problem: unknown): SeeChain;
340
+ /**
341
+ * Report a non-exception problem. Prefer passing a caught Error to `see()`
342
+ * when one exists. The name is a stable identifier (it participates in the
343
+ * issue fingerprint) — variable data goes in `.message()` or `.extras()`.
344
+ *
345
+ * ```ts
346
+ * if (results.length > LIMIT) {
347
+ * see.Violation("large query").causes_the("search results").to("be trimmed");
348
+ * }
349
+ * ```
350
+ */
351
+ Violation(name: string): SeeViolationChain;
352
+ /**
353
+ * Mark an exception as expected control flow — documents the expectation and
354
+ * reports nothing. The reason must start with "because".
355
+ */
356
+ ControlFlowException(err: unknown, because: string): void;
357
+ }
358
+ /**
359
+ * Structured error reporter — the whole grammar hangs off this one import.
360
+ * Safe to import anywhere; a call before `shipeasy({ serverKey })` warns and
361
+ * drops (never throws).
362
+ */
363
+ declare const see: SeeApi;
232
364
 
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 };
365
+ 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,247 @@ 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
+ var REPORTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-reported");
153
+ var SEE_MAX_CAUSE_DEPTH = 8;
154
+ function readReportStamp(err) {
155
+ if (typeof err !== "object" || err === null) return void 0;
156
+ const v = err[REPORTED_SYM];
157
+ return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
158
+ }
159
+ function findCausedBy(problem) {
160
+ let cur = problem;
161
+ const seen = /* @__PURE__ */ new Set();
162
+ for (let depth = 0; depth < SEE_MAX_CAUSE_DEPTH; depth++) {
163
+ if (typeof cur !== "object" || cur === null || seen.has(cur)) break;
164
+ seen.add(cur);
165
+ const stamp = readReportStamp(cur);
166
+ if (stamp) return stamp;
167
+ cur = cur.cause;
168
+ }
169
+ return void 0;
170
+ }
171
+ function markReported(problem, ev) {
172
+ if (!(problem instanceof Error)) return;
173
+ const stamp = {
174
+ error_type: ev.error_type,
175
+ message: ev.message,
176
+ subject: ev.subject,
177
+ outcome: ev.outcome
178
+ };
179
+ if (ev.stack !== void 0) stamp.stack = ev.stack;
180
+ try {
181
+ Object.defineProperty(problem, REPORTED_SYM, {
182
+ value: Object.freeze(stamp),
183
+ enumerable: false,
184
+ configurable: true,
185
+ writable: true
186
+ });
187
+ } catch {
188
+ }
189
+ }
190
+ function truncate(s, max) {
191
+ return s.length > max ? s.slice(0, max) : s;
192
+ }
193
+ function sanitizeExtras(extras) {
194
+ if (!extras || typeof extras !== "object") return void 0;
195
+ const out = {};
196
+ let n = 0;
197
+ for (const [k, v] of Object.entries(extras)) {
198
+ if (v === null || v === void 0) continue;
199
+ if (n >= SEE_MAX_EXTRA_KEYS) break;
200
+ if (typeof v === "string") out[k] = truncate(v, SEE_MAX_EXTRA_VALUE);
201
+ else if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
202
+ else if (typeof v === "boolean") out[k] = v;
203
+ else continue;
204
+ n += 1;
205
+ }
206
+ return n > 0 ? out : void 0;
207
+ }
208
+ function captureCallsiteStack() {
209
+ const raw = new Error().stack;
210
+ if (!raw) return void 0;
211
+ const lines = raw.split("\n");
212
+ const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
213
+ return kept.length ? kept.join("\n") : void 0;
214
+ }
215
+ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
216
+ let errorType;
217
+ let message;
218
+ let stack;
219
+ let kind;
220
+ if (isViolation(problem)) {
221
+ errorType = problem.violationName;
222
+ message = problem.violationMessage ?? problem.violationName;
223
+ stack = captureCallsiteStack();
224
+ kind = kindOverride ?? "violation";
225
+ } else if (problem instanceof Error) {
226
+ errorType = problem.name || "Error";
227
+ message = problem.message || String(problem);
228
+ stack = problem.stack ?? void 0;
229
+ kind = kindOverride ?? "caught";
230
+ } else {
231
+ errorType = "Error";
232
+ message = typeof problem === "string" ? problem : safeString(problem);
233
+ stack = captureCallsiteStack();
234
+ kind = kindOverride ?? "caught";
235
+ }
236
+ const ev = {
237
+ type: "error",
238
+ kind,
239
+ error_type: truncate(errorType, SEE_MAX_SUBJECT),
240
+ message: truncate(message, SEE_MAX_MESSAGE),
241
+ subject: consequence.subject,
242
+ outcome: consequence.outcome,
243
+ side: ctx.side,
244
+ sdk_version: ctx.sdkVersion,
245
+ ts: Date.now()
246
+ };
247
+ if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
248
+ const causedBy = findCausedBy(problem);
249
+ if (causedBy) ev.caused_by = causedBy;
250
+ const cleanExtras = sanitizeExtras(extras);
251
+ if (cleanExtras) ev.extras = cleanExtras;
252
+ if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
253
+ if (ctx.userId) ev.user_id = ctx.userId;
254
+ if (ctx.anonId) ev.anonymous_id = ctx.anonId;
255
+ if (ctx.env) ev.env = ctx.env;
256
+ markReported(problem, ev);
257
+ return ev;
258
+ }
259
+ function safeString(v) {
260
+ try {
261
+ return typeof v === "object" ? JSON.stringify(v) : String(v);
262
+ } catch {
263
+ return String(v);
264
+ }
265
+ }
266
+ var scheduleMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : (cb) => {
267
+ void Promise.resolve().then(cb);
268
+ };
269
+ function startSeeChain(getProblem, dispatch) {
270
+ let subject;
271
+ let outcome;
272
+ let collected;
273
+ let flushed = false;
274
+ scheduleMicrotask(() => {
275
+ if (flushed) return;
276
+ flushed = true;
277
+ dispatch(
278
+ getProblem(),
279
+ // Bare noun phrase — titles render as "… causes the {subject} …", so a
280
+ // leading article would double up ("causes the the app").
281
+ causesThe(subject ?? "app").to(outcome ?? "hit an error"),
282
+ collected
283
+ );
284
+ });
285
+ const tail = {
286
+ extras(x) {
287
+ if (x && typeof x === "object") collected = { ...collected, ...x };
288
+ return tail;
289
+ }
290
+ };
291
+ const step = {
292
+ to(o) {
293
+ outcome = String(o);
294
+ return tail;
295
+ }
296
+ };
297
+ const start = (s) => {
298
+ subject = String(s);
299
+ return step;
300
+ };
301
+ return { causes_the: start, causesThe: start };
302
+ }
303
+ function startSeeViolationChain(name, dispatch) {
304
+ let msg;
305
+ const base = startSeeChain(
306
+ () => msg !== void 0 ? violation(name).message(msg) : violation(name),
307
+ dispatch
308
+ );
309
+ const chain = {
310
+ ...base,
311
+ message(m) {
312
+ msg = String(m);
313
+ return chain;
314
+ }
315
+ };
316
+ return chain;
317
+ }
318
+ function topStackLine(stack) {
319
+ if (!stack) return "";
320
+ for (const line of stack.split("\n")) {
321
+ if (/^\s*at |@|:\d+:\d+/.test(line)) return line.trim().slice(0, 200);
322
+ }
323
+ return "";
324
+ }
325
+ var SeeLimiter = class {
326
+ constructor(maxPerSession = SEE_MAX_PER_SESSION, dedupWindowMs = SEE_DEDUP_WINDOW_MS) {
327
+ this.maxPerSession = maxPerSession;
328
+ this.dedupWindowMs = dedupWindowMs;
329
+ }
330
+ maxPerSession;
331
+ dedupWindowMs;
332
+ lastSent = /* @__PURE__ */ new Map();
333
+ sent = 0;
334
+ shouldSend(ev) {
335
+ if (this.sent >= this.maxPerSession) return false;
336
+ const key = `${ev.kind}|${ev.error_type}|${ev.message.slice(0, 200)}|${topStackLine(ev.stack)}`;
337
+ const now = Date.now();
338
+ const prev = this.lastSent.get(key);
339
+ if (prev !== void 0 && now - prev < this.dedupWindowMs) return false;
340
+ this.lastSent.set(key, now);
341
+ this.sent += 1;
342
+ return true;
343
+ }
344
+ };
345
+
106
346
  // src/server/index.ts
107
- var version = "1.0.0";
347
+ var version = "4.0.0";
108
348
  var C1 = 3432918353;
109
349
  var C2 = 461845907;
110
350
  function murmur3(key) {
@@ -254,6 +494,7 @@ var FlagsClient = class {
254
494
  baseUrl;
255
495
  env;
256
496
  telemetry;
497
+ seeLimiter = new SeeLimiter();
257
498
  flagsBlob = null;
258
499
  expsBlob = null;
259
500
  flagsEtag = null;
@@ -406,6 +647,27 @@ var FlagsClient = class {
406
647
  body
407
648
  }).catch((err) => console.warn("[shipeasy] track failed:", String(err)));
408
649
  }
650
+ /**
651
+ * Report a structured error into the errors primitive. Fire-and-forget —
652
+ * never blocks or throws into the request path. Spam-guarded by a 30s
653
+ * dedup window + per-process cap.
654
+ */
655
+ reportError(problem, consequence, extras, kind) {
656
+ try {
657
+ const ev = buildSeeEvent(problem, consequence, extras, {
658
+ side: "server",
659
+ sdkVersion: version,
660
+ env: this.env
661
+ }, kind);
662
+ if (!this.seeLimiter.shouldSend(ev)) return;
663
+ globalThis.fetch(`${this.baseUrl}/collect`, {
664
+ method: "POST",
665
+ headers: { "X-SDK-Key": this.apiKey, "Content-Type": "text/plain" },
666
+ body: JSON.stringify({ events: [ev] })
667
+ }).catch((err) => console.warn("[shipeasy] see() send failed:", String(err)));
668
+ } catch {
669
+ }
670
+ }
409
671
  /**
410
672
  * Evaluate all flags, configs, and experiments for a user against the locally
411
673
  * cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
@@ -714,6 +976,20 @@ var flags = {
714
976
  };
715
977
  }
716
978
  };
979
+ function dispatchSee(problem, consequence, extras, kind) {
980
+ if (!_server) {
981
+ console.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
982
+ return;
983
+ }
984
+ _server.reportError(problem, consequence, extras, kind);
985
+ }
986
+ var see = Object.assign(
987
+ (problem) => startSeeChain(() => problem, dispatchSee),
988
+ {
989
+ Violation: (name) => startSeeViolationChain(name, dispatchSee),
990
+ ControlFlowException: markExpected
991
+ }
992
+ );
717
993
  // Annotate the CommonJS export names for ESM import in node:
718
994
  0 && (module.exports = {
719
995
  FlagsClient,
@@ -724,6 +1000,7 @@ var flags = {
724
1000
  getBootstrapHtml,
725
1001
  getShipeasyServerClient,
726
1002
  i18n,
1003
+ see,
727
1004
  shipeasy,
728
1005
  version
729
1006
  });