@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,12 +38,270 @@ __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
  });
44
45
  module.exports = __toCommonJS(server_exports);
45
46
  var import_node_async_hooks = require("async_hooks");
46
- var version = "1.0.0";
47
+
48
+ // src/telemetry.ts
49
+ async function sha256Hex(input) {
50
+ const buf = new TextEncoder().encode(input);
51
+ const digest = await crypto.subtle.digest("SHA-256", buf);
52
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
53
+ }
54
+ var DEFAULT_TELEMETRY_URL = "https://t.shipeasy.ai";
55
+ var Telemetry = class {
56
+ prefix;
57
+ disabled;
58
+ dedupeMs;
59
+ // Last-emit timestamp per `feature/resource`, for the dedup window. Bounded by
60
+ // the number of distinct keys the app reads.
61
+ lastEmit = /* @__PURE__ */ new Map();
62
+ // Resolved once at construction and reused by every emit(), so the per-eval
63
+ // cost is a Map-free microtask, not a hash.
64
+ keyHash;
65
+ constructor(opts) {
66
+ const endpoint = (opts.endpoint ?? "").replace(/\/$/, "");
67
+ this.disabled = opts.disabled === true || !opts.sdkKey || !endpoint;
68
+ this.dedupeMs = opts.dedupeMs ?? 2e3;
69
+ this.prefix = `${endpoint}/t`;
70
+ this.keyHash = this.disabled ? null : sha256Hex(opts.sdkKey).then((h) => `${h}/${opts.side}/${encodeURIComponent(opts.env)}`).catch(() => "");
71
+ }
72
+ /**
73
+ * Emit a single best-effort usage beacon for one evaluation. Never blocks the
74
+ * caller (the hash is already resolved) and never throws — a failed beacon
75
+ * must never affect the evaluation it measures.
76
+ */
77
+ emit(feature, resource) {
78
+ if (this.disabled || !this.keyHash) return;
79
+ if (this.dedupeMs > 0) {
80
+ const dedupeKey = `${feature}/${resource}`;
81
+ const now = Date.now();
82
+ const last = this.lastEmit.get(dedupeKey);
83
+ if (last !== void 0 && now - last < this.dedupeMs) return;
84
+ this.lastEmit.set(dedupeKey, now);
85
+ }
86
+ void this.keyHash.then((suffix) => {
87
+ if (!suffix) return;
88
+ send(`${this.prefix}/${suffix}/${feature}/${encodeURIComponent(resource)}`);
89
+ });
90
+ }
91
+ };
92
+ function send(url) {
93
+ try {
94
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
95
+ navigator.sendBeacon(url);
96
+ return;
97
+ }
98
+ const f = globalThis.fetch;
99
+ if (typeof f === "function") {
100
+ void f(url, { method: "GET", keepalive: true }).catch(() => {
101
+ });
102
+ }
103
+ } catch {
104
+ }
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
+
303
+ // src/server/index.ts
304
+ var version = "4.0.0";
47
305
  var C1 = 3432918353;
48
306
  var C2 = 461845907;
49
307
  function murmur3(key) {
@@ -192,6 +450,8 @@ var FlagsClient = class {
192
450
  apiKey;
193
451
  baseUrl;
194
452
  env;
453
+ telemetry;
454
+ seeLimiter = new SeeLimiter();
195
455
  flagsBlob = null;
196
456
  expsBlob = null;
197
457
  flagsEtag = null;
@@ -203,6 +463,13 @@ var FlagsClient = class {
203
463
  this.apiKey = opts.apiKey;
204
464
  this.baseUrl = (opts.baseUrl ?? "https://cdn.shipeasy.ai").replace(/\/$/, "");
205
465
  this.env = opts.env ?? "prod";
466
+ this.telemetry = new Telemetry({
467
+ endpoint: opts.telemetryUrl ?? DEFAULT_TELEMETRY_URL,
468
+ sdkKey: this.apiKey,
469
+ side: "server",
470
+ env: this.env,
471
+ disabled: opts.disableTelemetry
472
+ });
206
473
  if (opts.initialBlob) {
207
474
  this.flagsBlob = opts.initialBlob;
208
475
  this.initialized = true;
@@ -264,17 +531,20 @@ var FlagsClient = class {
264
531
  this.expsBlob = await res.json();
265
532
  }
266
533
  getFlag(name, user) {
534
+ this.telemetry.emit("gate", name);
267
535
  const gate = this.flagsBlob?.gates[name];
268
536
  if (!gate) return false;
269
537
  return evalGateInternal(gate, user);
270
538
  }
271
539
  getConfig(name, decode) {
540
+ this.telemetry.emit("config", name);
272
541
  const entry = this.flagsBlob?.configs[name];
273
542
  if (!entry) return void 0;
274
543
  if (!decode) return entry.value;
275
544
  return decode(entry.value);
276
545
  }
277
546
  getExperiment(name, user, defaultParams, decode) {
547
+ this.telemetry.emit("experiment", name);
278
548
  const notIn = {
279
549
  inExperiment: false,
280
550
  group: "control",
@@ -334,6 +604,27 @@ var FlagsClient = class {
334
604
  body
335
605
  }).catch((err) => console.warn("[shipeasy] track failed:", String(err)));
336
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
+ }
337
628
  /**
338
629
  * Evaluate all flags, configs, and experiments for a user against the locally
339
630
  * cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
@@ -349,15 +640,18 @@ var FlagsClient = class {
349
640
  const experiments = {};
350
641
  const killswitches = {};
351
642
  for (const [name, gate] of Object.entries(this.flagsBlob?.gates ?? {})) {
643
+ this.telemetry.emit("gate", name);
352
644
  flags2[name] = evalGateInternal(gate, user);
353
645
  }
354
646
  for (const [name, entry] of Object.entries(this.flagsBlob?.configs ?? {})) {
647
+ this.telemetry.emit("config", name);
355
648
  configs[name] = entry.value;
356
649
  }
357
650
  for (const [name] of Object.entries(this.expsBlob?.experiments ?? {})) {
358
651
  experiments[name] = this.getExperiment(name, user, {});
359
652
  }
360
653
  for (const [name, ks] of Object.entries(this.flagsBlob?.killswitches ?? {})) {
654
+ this.telemetry.emit("ks", name);
361
655
  if (ks.switches && Object.keys(ks.switches).length > 0) {
362
656
  const out = {};
363
657
  for (const [k, v] of Object.entries(ks.switches)) out[k] = isEnabled(v);
@@ -377,6 +671,7 @@ var FlagsClient = class {
377
671
  return { flags: flags2, configs, experiments, killswitches };
378
672
  }
379
673
  getKillswitch(name, switchKey) {
674
+ this.telemetry.emit("ks", name);
380
675
  const ks = this.flagsBlob?.killswitches?.[name];
381
676
  if (!ks) return false;
382
677
  if (switchKey === void 0) return isEnabled(ks.killed);
@@ -512,7 +807,7 @@ async function shipeasy(opts) {
512
807
  );
513
808
  }
514
809
  const profile = opts.i18nDefaultProfile ?? "en:prod";
515
- flags.configure({ apiKey: serverKey });
810
+ flags.configure({ apiKey: serverKey, disableTelemetry: opts.disableTelemetry });
516
811
  let resolvedUrlOverrides = opts.urlOverrides;
517
812
  if (!resolvedUrlOverrides) {
518
813
  try {
@@ -638,6 +933,20 @@ var flags = {
638
933
  };
639
934
  }
640
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
+ );
641
950
  // Annotate the CommonJS export names for ESM import in node:
642
951
  0 && (module.exports = {
643
952
  FlagsClient,
@@ -648,6 +957,7 @@ var flags = {
648
957
  getBootstrapHtml,
649
958
  getShipeasyServerClient,
650
959
  i18n,
960
+ see,
651
961
  shipeasy,
652
962
  version
653
963
  });