@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.
@@ -60,8 +60,247 @@ 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
+ var REPORTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-reported");
109
+ var SEE_MAX_CAUSE_DEPTH = 8;
110
+ function readReportStamp(err) {
111
+ if (typeof err !== "object" || err === null) return void 0;
112
+ const v = err[REPORTED_SYM];
113
+ return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
114
+ }
115
+ function findCausedBy(problem) {
116
+ let cur = problem;
117
+ const seen = /* @__PURE__ */ new Set();
118
+ for (let depth = 0; depth < SEE_MAX_CAUSE_DEPTH; depth++) {
119
+ if (typeof cur !== "object" || cur === null || seen.has(cur)) break;
120
+ seen.add(cur);
121
+ const stamp = readReportStamp(cur);
122
+ if (stamp) return stamp;
123
+ cur = cur.cause;
124
+ }
125
+ return void 0;
126
+ }
127
+ function markReported(problem, ev) {
128
+ if (!(problem instanceof Error)) return;
129
+ const stamp = {
130
+ error_type: ev.error_type,
131
+ message: ev.message,
132
+ subject: ev.subject,
133
+ outcome: ev.outcome
134
+ };
135
+ if (ev.stack !== void 0) stamp.stack = ev.stack;
136
+ try {
137
+ Object.defineProperty(problem, REPORTED_SYM, {
138
+ value: Object.freeze(stamp),
139
+ enumerable: false,
140
+ configurable: true,
141
+ writable: true
142
+ });
143
+ } catch {
144
+ }
145
+ }
146
+ function truncate(s, max) {
147
+ return s.length > max ? s.slice(0, max) : s;
148
+ }
149
+ function sanitizeExtras(extras) {
150
+ if (!extras || typeof extras !== "object") return void 0;
151
+ const out = {};
152
+ let n = 0;
153
+ for (const [k, v] of Object.entries(extras)) {
154
+ if (v === null || v === void 0) continue;
155
+ if (n >= SEE_MAX_EXTRA_KEYS) break;
156
+ if (typeof v === "string") out[k] = truncate(v, SEE_MAX_EXTRA_VALUE);
157
+ else if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
158
+ else if (typeof v === "boolean") out[k] = v;
159
+ else continue;
160
+ n += 1;
161
+ }
162
+ return n > 0 ? out : void 0;
163
+ }
164
+ function captureCallsiteStack() {
165
+ const raw = new Error().stack;
166
+ if (!raw) return void 0;
167
+ const lines = raw.split("\n");
168
+ const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
169
+ return kept.length ? kept.join("\n") : void 0;
170
+ }
171
+ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
172
+ let errorType;
173
+ let message;
174
+ let stack;
175
+ let kind;
176
+ if (isViolation(problem)) {
177
+ errorType = problem.violationName;
178
+ message = problem.violationMessage ?? problem.violationName;
179
+ stack = captureCallsiteStack();
180
+ kind = kindOverride ?? "violation";
181
+ } else if (problem instanceof Error) {
182
+ errorType = problem.name || "Error";
183
+ message = problem.message || String(problem);
184
+ stack = problem.stack ?? void 0;
185
+ kind = kindOverride ?? "caught";
186
+ } else {
187
+ errorType = "Error";
188
+ message = typeof problem === "string" ? problem : safeString(problem);
189
+ stack = captureCallsiteStack();
190
+ kind = kindOverride ?? "caught";
191
+ }
192
+ const ev = {
193
+ type: "error",
194
+ kind,
195
+ error_type: truncate(errorType, SEE_MAX_SUBJECT),
196
+ message: truncate(message, SEE_MAX_MESSAGE),
197
+ subject: consequence.subject,
198
+ outcome: consequence.outcome,
199
+ side: ctx.side,
200
+ sdk_version: ctx.sdkVersion,
201
+ ts: Date.now()
202
+ };
203
+ if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
204
+ const causedBy = findCausedBy(problem);
205
+ if (causedBy) ev.caused_by = causedBy;
206
+ const cleanExtras = sanitizeExtras(extras);
207
+ if (cleanExtras) ev.extras = cleanExtras;
208
+ if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
209
+ if (ctx.userId) ev.user_id = ctx.userId;
210
+ if (ctx.anonId) ev.anonymous_id = ctx.anonId;
211
+ if (ctx.env) ev.env = ctx.env;
212
+ markReported(problem, ev);
213
+ return ev;
214
+ }
215
+ function safeString(v) {
216
+ try {
217
+ return typeof v === "object" ? JSON.stringify(v) : String(v);
218
+ } catch {
219
+ return String(v);
220
+ }
221
+ }
222
+ var scheduleMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : (cb) => {
223
+ void Promise.resolve().then(cb);
224
+ };
225
+ function startSeeChain(getProblem, dispatch) {
226
+ let subject;
227
+ let outcome;
228
+ let collected;
229
+ let flushed = false;
230
+ scheduleMicrotask(() => {
231
+ if (flushed) return;
232
+ flushed = true;
233
+ dispatch(
234
+ getProblem(),
235
+ // Bare noun phrase — titles render as "… causes the {subject} …", so a
236
+ // leading article would double up ("causes the the app").
237
+ causesThe(subject ?? "app").to(outcome ?? "hit an error"),
238
+ collected
239
+ );
240
+ });
241
+ const tail = {
242
+ extras(x) {
243
+ if (x && typeof x === "object") collected = { ...collected, ...x };
244
+ return tail;
245
+ }
246
+ };
247
+ const step = {
248
+ to(o) {
249
+ outcome = String(o);
250
+ return tail;
251
+ }
252
+ };
253
+ const start = (s) => {
254
+ subject = String(s);
255
+ return step;
256
+ };
257
+ return { causes_the: start, causesThe: start };
258
+ }
259
+ function startSeeViolationChain(name, dispatch) {
260
+ let msg;
261
+ const base = startSeeChain(
262
+ () => msg !== void 0 ? violation(name).message(msg) : violation(name),
263
+ dispatch
264
+ );
265
+ const chain = {
266
+ ...base,
267
+ message(m) {
268
+ msg = String(m);
269
+ return chain;
270
+ }
271
+ };
272
+ return chain;
273
+ }
274
+ function topStackLine(stack) {
275
+ if (!stack) return "";
276
+ for (const line of stack.split("\n")) {
277
+ if (/^\s*at |@|:\d+:\d+/.test(line)) return line.trim().slice(0, 200);
278
+ }
279
+ return "";
280
+ }
281
+ var SeeLimiter = class {
282
+ constructor(maxPerSession = SEE_MAX_PER_SESSION, dedupWindowMs = SEE_DEDUP_WINDOW_MS) {
283
+ this.maxPerSession = maxPerSession;
284
+ this.dedupWindowMs = dedupWindowMs;
285
+ }
286
+ maxPerSession;
287
+ dedupWindowMs;
288
+ lastSent = /* @__PURE__ */ new Map();
289
+ sent = 0;
290
+ shouldSend(ev) {
291
+ if (this.sent >= this.maxPerSession) return false;
292
+ const key = `${ev.kind}|${ev.error_type}|${ev.message.slice(0, 200)}|${topStackLine(ev.stack)}`;
293
+ const now = Date.now();
294
+ const prev = this.lastSent.get(key);
295
+ if (prev !== void 0 && now - prev < this.dedupWindowMs) return false;
296
+ this.lastSent.set(key, now);
297
+ this.sent += 1;
298
+ return true;
299
+ }
300
+ };
301
+
63
302
  // src/server/index.ts
64
- var version = "1.0.0";
303
+ var version = "4.0.0";
65
304
  var C1 = 3432918353;
66
305
  var C2 = 461845907;
67
306
  function murmur3(key) {
@@ -211,6 +450,7 @@ var FlagsClient = class {
211
450
  baseUrl;
212
451
  env;
213
452
  telemetry;
453
+ seeLimiter = new SeeLimiter();
214
454
  flagsBlob = null;
215
455
  expsBlob = null;
216
456
  flagsEtag = null;
@@ -363,6 +603,27 @@ var FlagsClient = class {
363
603
  body
364
604
  }).catch((err) => console.warn("[shipeasy] track failed:", String(err)));
365
605
  }
606
+ /**
607
+ * Report a structured error into the errors primitive. Fire-and-forget —
608
+ * never blocks or throws into the request path. Spam-guarded by a 30s
609
+ * dedup window + per-process cap.
610
+ */
611
+ reportError(problem, consequence, extras, kind) {
612
+ try {
613
+ const ev = buildSeeEvent(problem, consequence, extras, {
614
+ side: "server",
615
+ sdkVersion: version,
616
+ env: this.env
617
+ }, kind);
618
+ if (!this.seeLimiter.shouldSend(ev)) return;
619
+ globalThis.fetch(`${this.baseUrl}/collect`, {
620
+ method: "POST",
621
+ headers: { "X-SDK-Key": this.apiKey, "Content-Type": "text/plain" },
622
+ body: JSON.stringify({ events: [ev] })
623
+ }).catch((err) => console.warn("[shipeasy] see() send failed:", String(err)));
624
+ } catch {
625
+ }
626
+ }
366
627
  /**
367
628
  * Evaluate all flags, configs, and experiments for a user against the locally
368
629
  * cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
@@ -671,6 +932,20 @@ var flags = {
671
932
  };
672
933
  }
673
934
  };
935
+ function dispatchSee(problem, consequence, extras, kind) {
936
+ if (!_server) {
937
+ console.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
938
+ return;
939
+ }
940
+ _server.reportError(problem, consequence, extras, kind);
941
+ }
942
+ var see = Object.assign(
943
+ (problem) => startSeeChain(() => problem, dispatchSee),
944
+ {
945
+ Violation: (name) => startSeeViolationChain(name, dispatchSee),
946
+ ControlFlowException: markExpected
947
+ }
948
+ );
674
949
  export {
675
950
  FlagsClient,
676
951
  _resetShipeasyServerForTests,
@@ -680,6 +955,7 @@ export {
680
955
  getBootstrapHtml,
681
956
  getShipeasyServerClient,
682
957
  i18n,
958
+ see,
683
959
  shipeasy,
684
960
  version
685
961
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipeasy/sdk",
3
- "version": "3.1.0",
3
+ "version": "4.2.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",