@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.
- package/README.md +52 -0
- package/dist/client/index.d.mts +149 -7
- package/dist/client/index.d.ts +149 -7
- package/dist/client/index.js +321 -50
- package/dist/client/index.mjs +320 -50
- package/dist/server/index.d.mts +112 -2
- package/dist/server/index.d.ts +112 -2
- package/dist/server/index.js +235 -1
- package/dist/server/index.mjs +234 -1
- package/package.json +1 -1
package/dist/server/index.d.ts
CHANGED
|
@@ -1,4 +1,63 @@
|
|
|
1
|
-
|
|
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 };
|
package/dist/server/index.js
CHANGED
|
@@ -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 = "
|
|
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
|
});
|
package/dist/server/index.mjs
CHANGED
|
@@ -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 = "
|
|
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
|
+
"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",
|