@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.
- package/README.md +52 -0
- package/dist/client/index.d.mts +165 -7
- package/dist/client/index.d.ts +165 -7
- package/dist/client/index.js +396 -51
- package/dist/client/index.mjs +393 -51
- package/dist/server/index.d.mts +130 -2
- package/dist/server/index.d.ts +130 -2
- package/dist/server/index.js +312 -2
- package/dist/server/index.mjs +311 -2
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
});
|