@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/client/index.mjs
CHANGED
|
@@ -57,8 +57,208 @@ function send(url) {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// src/see/core.ts
|
|
61
|
+
var SEE_MAX_MESSAGE = 500;
|
|
62
|
+
var SEE_MAX_STACK = 8e3;
|
|
63
|
+
var SEE_MAX_SUBJECT = 200;
|
|
64
|
+
var SEE_MAX_EXTRA_VALUE = 200;
|
|
65
|
+
var SEE_MAX_EXTRA_KEYS = 20;
|
|
66
|
+
var SEE_DEDUP_WINDOW_MS = 3e4;
|
|
67
|
+
var SEE_MAX_PER_SESSION = 25;
|
|
68
|
+
function causesThe(subject) {
|
|
69
|
+
return {
|
|
70
|
+
to(outcome) {
|
|
71
|
+
return {
|
|
72
|
+
__seConsequence: true,
|
|
73
|
+
subject: truncate(String(subject), SEE_MAX_SUBJECT),
|
|
74
|
+
outcome: truncate(String(outcome), SEE_MAX_SUBJECT)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function violation(name) {
|
|
80
|
+
const make = (msg) => ({
|
|
81
|
+
__seViolation: true,
|
|
82
|
+
violationName: String(name),
|
|
83
|
+
...msg !== void 0 ? { violationMessage: msg } : {},
|
|
84
|
+
message(m) {
|
|
85
|
+
return make(String(m));
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return make();
|
|
89
|
+
}
|
|
90
|
+
function isViolation(p) {
|
|
91
|
+
return typeof p === "object" && p !== null && p.__seViolation === true;
|
|
92
|
+
}
|
|
93
|
+
var EXPECTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-expected");
|
|
94
|
+
function markExpected(err, because) {
|
|
95
|
+
if (typeof err !== "object" || err === null) return;
|
|
96
|
+
try {
|
|
97
|
+
Object.defineProperty(err, EXPECTED_SYM, {
|
|
98
|
+
value: String(because),
|
|
99
|
+
enumerable: false,
|
|
100
|
+
configurable: true
|
|
101
|
+
});
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function isExpected(err) {
|
|
106
|
+
if (typeof err !== "object" || err === null) return false;
|
|
107
|
+
return err[EXPECTED_SYM] !== void 0;
|
|
108
|
+
}
|
|
109
|
+
function truncate(s, max) {
|
|
110
|
+
return s.length > max ? s.slice(0, max) : s;
|
|
111
|
+
}
|
|
112
|
+
function sanitizeExtras(extras) {
|
|
113
|
+
if (!extras || typeof extras !== "object") return void 0;
|
|
114
|
+
const out = {};
|
|
115
|
+
let n = 0;
|
|
116
|
+
for (const [k, v] of Object.entries(extras)) {
|
|
117
|
+
if (v === null || v === void 0) continue;
|
|
118
|
+
if (n >= SEE_MAX_EXTRA_KEYS) break;
|
|
119
|
+
if (typeof v === "string") out[k] = truncate(v, SEE_MAX_EXTRA_VALUE);
|
|
120
|
+
else if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
|
|
121
|
+
else if (typeof v === "boolean") out[k] = v;
|
|
122
|
+
else continue;
|
|
123
|
+
n += 1;
|
|
124
|
+
}
|
|
125
|
+
return n > 0 ? out : void 0;
|
|
126
|
+
}
|
|
127
|
+
function captureCallsiteStack() {
|
|
128
|
+
const raw = new Error().stack;
|
|
129
|
+
if (!raw) return void 0;
|
|
130
|
+
const lines = raw.split("\n");
|
|
131
|
+
const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
|
|
132
|
+
return kept.length ? kept.join("\n") : void 0;
|
|
133
|
+
}
|
|
134
|
+
function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
|
|
135
|
+
let errorType;
|
|
136
|
+
let message;
|
|
137
|
+
let stack;
|
|
138
|
+
let kind;
|
|
139
|
+
if (isViolation(problem)) {
|
|
140
|
+
errorType = problem.violationName;
|
|
141
|
+
message = problem.violationMessage ?? problem.violationName;
|
|
142
|
+
stack = captureCallsiteStack();
|
|
143
|
+
kind = kindOverride ?? "violation";
|
|
144
|
+
} else if (problem instanceof Error) {
|
|
145
|
+
errorType = problem.name || "Error";
|
|
146
|
+
message = problem.message || String(problem);
|
|
147
|
+
stack = problem.stack ?? void 0;
|
|
148
|
+
kind = kindOverride ?? "caught";
|
|
149
|
+
} else {
|
|
150
|
+
errorType = "Error";
|
|
151
|
+
message = typeof problem === "string" ? problem : safeString(problem);
|
|
152
|
+
stack = captureCallsiteStack();
|
|
153
|
+
kind = kindOverride ?? "caught";
|
|
154
|
+
}
|
|
155
|
+
const ev = {
|
|
156
|
+
type: "error",
|
|
157
|
+
kind,
|
|
158
|
+
error_type: truncate(errorType, SEE_MAX_SUBJECT),
|
|
159
|
+
message: truncate(message, SEE_MAX_MESSAGE),
|
|
160
|
+
subject: consequence.subject,
|
|
161
|
+
outcome: consequence.outcome,
|
|
162
|
+
side: ctx.side,
|
|
163
|
+
sdk_version: ctx.sdkVersion,
|
|
164
|
+
ts: Date.now()
|
|
165
|
+
};
|
|
166
|
+
if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
|
|
167
|
+
const cleanExtras = sanitizeExtras(extras);
|
|
168
|
+
if (cleanExtras) ev.extras = cleanExtras;
|
|
169
|
+
if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
|
|
170
|
+
if (ctx.userId) ev.user_id = ctx.userId;
|
|
171
|
+
if (ctx.anonId) ev.anonymous_id = ctx.anonId;
|
|
172
|
+
if (ctx.env) ev.env = ctx.env;
|
|
173
|
+
return ev;
|
|
174
|
+
}
|
|
175
|
+
function safeString(v) {
|
|
176
|
+
try {
|
|
177
|
+
return typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
178
|
+
} catch {
|
|
179
|
+
return String(v);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
var scheduleMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : (cb) => {
|
|
183
|
+
void Promise.resolve().then(cb);
|
|
184
|
+
};
|
|
185
|
+
function startSeeChain(getProblem, dispatch) {
|
|
186
|
+
let subject;
|
|
187
|
+
let outcome;
|
|
188
|
+
let collected;
|
|
189
|
+
let flushed = false;
|
|
190
|
+
scheduleMicrotask(() => {
|
|
191
|
+
if (flushed) return;
|
|
192
|
+
flushed = true;
|
|
193
|
+
dispatch(
|
|
194
|
+
getProblem(),
|
|
195
|
+
causesThe(subject ?? "the app").to(outcome ?? "hit an error"),
|
|
196
|
+
collected
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
const tail = {
|
|
200
|
+
extras(x) {
|
|
201
|
+
if (x && typeof x === "object") collected = { ...collected, ...x };
|
|
202
|
+
return tail;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const step = {
|
|
206
|
+
to(o) {
|
|
207
|
+
outcome = String(o);
|
|
208
|
+
return tail;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const start = (s) => {
|
|
212
|
+
subject = String(s);
|
|
213
|
+
return step;
|
|
214
|
+
};
|
|
215
|
+
return { causes_the: start, causesThe: start };
|
|
216
|
+
}
|
|
217
|
+
function startSeeViolationChain(name, dispatch) {
|
|
218
|
+
let msg;
|
|
219
|
+
const base = startSeeChain(
|
|
220
|
+
() => msg !== void 0 ? violation(name).message(msg) : violation(name),
|
|
221
|
+
dispatch
|
|
222
|
+
);
|
|
223
|
+
const chain = {
|
|
224
|
+
...base,
|
|
225
|
+
message(m) {
|
|
226
|
+
msg = String(m);
|
|
227
|
+
return chain;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
return chain;
|
|
231
|
+
}
|
|
232
|
+
function topStackLine(stack) {
|
|
233
|
+
if (!stack) return "";
|
|
234
|
+
for (const line of stack.split("\n")) {
|
|
235
|
+
if (/^\s*at |@|:\d+:\d+/.test(line)) return line.trim().slice(0, 200);
|
|
236
|
+
}
|
|
237
|
+
return "";
|
|
238
|
+
}
|
|
239
|
+
var SeeLimiter = class {
|
|
240
|
+
constructor(maxPerSession = SEE_MAX_PER_SESSION, dedupWindowMs = SEE_DEDUP_WINDOW_MS) {
|
|
241
|
+
this.maxPerSession = maxPerSession;
|
|
242
|
+
this.dedupWindowMs = dedupWindowMs;
|
|
243
|
+
}
|
|
244
|
+
maxPerSession;
|
|
245
|
+
dedupWindowMs;
|
|
246
|
+
lastSent = /* @__PURE__ */ new Map();
|
|
247
|
+
sent = 0;
|
|
248
|
+
shouldSend(ev) {
|
|
249
|
+
if (this.sent >= this.maxPerSession) return false;
|
|
250
|
+
const key = `${ev.kind}|${ev.error_type}|${ev.message.slice(0, 200)}|${topStackLine(ev.stack)}`;
|
|
251
|
+
const now = Date.now();
|
|
252
|
+
const prev = this.lastSent.get(key);
|
|
253
|
+
if (prev !== void 0 && now - prev < this.dedupWindowMs) return false;
|
|
254
|
+
this.lastSent.set(key, now);
|
|
255
|
+
this.sent += 1;
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
60
260
|
// src/client/index.ts
|
|
61
|
-
var version = "
|
|
261
|
+
var version = "4.0.0";
|
|
62
262
|
var FLUSH_INTERVAL_MS = 5e3;
|
|
63
263
|
var MAX_BUFFER = 100;
|
|
64
264
|
var ANON_ID_KEY = "__se_anon_id";
|
|
@@ -92,6 +292,13 @@ var EventBuffer = class {
|
|
|
92
292
|
this.timer = null;
|
|
93
293
|
}
|
|
94
294
|
}
|
|
295
|
+
/** True once this visitor has been exposed to ≥1 experiment (this tab or a
|
|
296
|
+
* prior page in the session — the dedup set persists in sessionStorage).
|
|
297
|
+
* Gates auto-metric emission: vitals from non-participants are never read
|
|
298
|
+
* by the analysis pipeline and would be pure AE write cost (see cost.md). */
|
|
299
|
+
hasExposures() {
|
|
300
|
+
return this.exposureSeen.size > 0;
|
|
301
|
+
}
|
|
95
302
|
pushExposure(experiment, group, userId, anonId) {
|
|
96
303
|
const key = `${userId || anonId}:${experiment}`;
|
|
97
304
|
if (this.exposureSeen.has(key)) return;
|
|
@@ -157,16 +364,29 @@ var EventBuffer = class {
|
|
|
157
364
|
flush(useBeacon = false) {
|
|
158
365
|
if (!this.queue.length) return;
|
|
159
366
|
const batch = this.queue.splice(0);
|
|
160
|
-
|
|
367
|
+
this.send(batch, useBeacon);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Bypass the 5s queue and ship events immediately — used by see() error
|
|
371
|
+
* reporting so occurrences land near-real-time and survive page unload.
|
|
372
|
+
* Beacon-first (fire-and-forget, unload-safe), keepalive fetch fallback.
|
|
373
|
+
*/
|
|
374
|
+
sendNow(events) {
|
|
375
|
+
this.send(events, true);
|
|
376
|
+
}
|
|
377
|
+
send(batch, useBeacon) {
|
|
161
378
|
if (useBeacon && typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
162
379
|
const beaconBody = JSON.stringify({ k: this.sdkKey, events: batch });
|
|
163
|
-
|
|
164
|
-
|
|
380
|
+
try {
|
|
381
|
+
if (navigator.sendBeacon(this.collectUrl, new Blob([beaconBody], { type: "text/plain" })))
|
|
382
|
+
return;
|
|
383
|
+
} catch {
|
|
384
|
+
}
|
|
165
385
|
}
|
|
166
386
|
fetch(this.collectUrl, {
|
|
167
387
|
method: "POST",
|
|
168
388
|
headers: { "X-SDK-Key": this.sdkKey, "Content-Type": "application/json" },
|
|
169
|
-
body,
|
|
389
|
+
body: JSON.stringify({ events: batch }),
|
|
170
390
|
keepalive: true
|
|
171
391
|
}).catch(() => {
|
|
172
392
|
});
|
|
@@ -183,14 +403,12 @@ var EventBuffer = class {
|
|
|
183
403
|
});
|
|
184
404
|
}
|
|
185
405
|
};
|
|
186
|
-
|
|
187
|
-
function installAutoGuardrails(buffer, userId, anonId, groups) {
|
|
406
|
+
function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignoreUrlPrefixes, always = false) {
|
|
188
407
|
if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") return;
|
|
408
|
+
const shouldEmit = () => always || buffer.hasExposures();
|
|
189
409
|
let lcp = null;
|
|
190
410
|
let inp = null;
|
|
191
411
|
let clsBad = false;
|
|
192
|
-
let jsErrorCount = 0;
|
|
193
|
-
let netErrorCount = 0;
|
|
194
412
|
let navTimingFlushed = false;
|
|
195
413
|
if (groups.vitals) {
|
|
196
414
|
try {
|
|
@@ -229,68 +447,71 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
|
|
|
229
447
|
if (groups.errors) {
|
|
230
448
|
const origOnError = window.onerror;
|
|
231
449
|
window.onerror = (msg, source, lineno, _colno, err) => {
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
450
|
+
if (!isExpected(err)) {
|
|
451
|
+
const problem = err ?? (typeof msg === "string" && msg ? msg : "Unknown error");
|
|
452
|
+
reportSee(
|
|
453
|
+
problem,
|
|
454
|
+
causesThe("the page").to("hit an unhandled error"),
|
|
455
|
+
{
|
|
456
|
+
source: typeof source === "string" ? source : void 0,
|
|
457
|
+
line: lineno ?? void 0
|
|
458
|
+
},
|
|
459
|
+
"uncaught"
|
|
460
|
+
);
|
|
241
461
|
}
|
|
242
462
|
if (typeof origOnError === "function") return origOnError(msg, source, lineno, _colno, err);
|
|
243
463
|
return false;
|
|
244
464
|
};
|
|
245
465
|
window.addEventListener("unhandledrejection", (e) => {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
});
|
|
255
|
-
}
|
|
466
|
+
const reason = e.reason;
|
|
467
|
+
if (isExpected(reason)) return;
|
|
468
|
+
reportSee(
|
|
469
|
+
reason ?? "Unhandled promise rejection",
|
|
470
|
+
causesThe("the page").to("hit an unhandled promise rejection"),
|
|
471
|
+
void 0,
|
|
472
|
+
"unhandled_rejection"
|
|
473
|
+
);
|
|
256
474
|
});
|
|
257
475
|
const origFetch = window.fetch;
|
|
258
476
|
window.fetch = async function(...args) {
|
|
259
477
|
const startedAt = typeof performance !== "undefined" ? performance.now() : 0;
|
|
260
478
|
const url = typeof args[0] === "string" ? args[0] : args[0].toString();
|
|
479
|
+
const ignored = ignoreUrlPrefixes.some((p) => p && url.startsWith(p));
|
|
480
|
+
const bareUrl = url.split("?")[0].slice(0, 200);
|
|
261
481
|
let res;
|
|
262
482
|
try {
|
|
263
483
|
res = await origFetch.apply(this, args);
|
|
264
484
|
} catch (err) {
|
|
265
|
-
if (
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
});
|
|
485
|
+
if (!ignored && !isExpected(err)) {
|
|
486
|
+
reportSee(
|
|
487
|
+
violation("NetworkError").message(`request to ${bareUrl} failed`),
|
|
488
|
+
causesThe("a network request").to("fail without a response"),
|
|
489
|
+
{ status: 0, url: url.slice(0, 200) },
|
|
490
|
+
"network"
|
|
491
|
+
);
|
|
273
492
|
}
|
|
274
493
|
throw err;
|
|
275
494
|
}
|
|
276
|
-
if (res.status >= 500
|
|
277
|
-
netErrorCount += 1;
|
|
495
|
+
if (!ignored && res.status >= 500) {
|
|
278
496
|
const elapsed = typeof performance !== "undefined" ? performance.now() - startedAt : 0;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
status: res.status,
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
});
|
|
497
|
+
reportSee(
|
|
498
|
+
violation("Http5xx").message(`request to ${bareUrl} returned ${res.status}`),
|
|
499
|
+
causesThe("a network request").to(`fail with HTTP ${res.status}`),
|
|
500
|
+
{ status: res.status, url: url.slice(0, 200), duration_ms: Math.round(elapsed) },
|
|
501
|
+
"network"
|
|
502
|
+
);
|
|
286
503
|
}
|
|
287
504
|
return res;
|
|
288
505
|
};
|
|
289
506
|
}
|
|
290
507
|
const flushNavTiming = () => {
|
|
291
508
|
if (navTimingFlushed) return;
|
|
509
|
+
if (!groups.vitals) {
|
|
510
|
+
navTimingFlushed = true;
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (!shouldEmit()) return;
|
|
292
514
|
navTimingFlushed = true;
|
|
293
|
-
if (!groups.vitals) return;
|
|
294
515
|
try {
|
|
295
516
|
const navList = performance.getEntriesByType("navigation");
|
|
296
517
|
const nav = navList[0];
|
|
@@ -325,7 +546,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
|
|
|
325
546
|
};
|
|
326
547
|
if (groups.engagement) {
|
|
327
548
|
try {
|
|
328
|
-
buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
|
|
549
|
+
if (shouldEmit()) buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
|
|
329
550
|
} catch {
|
|
330
551
|
}
|
|
331
552
|
let lastEmit = Date.now();
|
|
@@ -333,6 +554,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
|
|
|
333
554
|
document.addEventListener("visibilitychange", () => {
|
|
334
555
|
if (document.visibilityState !== "visible") return;
|
|
335
556
|
if (Date.now() - lastEmit < SESSION_GAP_MS) return;
|
|
557
|
+
if (!shouldEmit()) return;
|
|
336
558
|
try {
|
|
337
559
|
buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
|
|
338
560
|
lastEmit = Date.now();
|
|
@@ -355,7 +577,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
|
|
|
355
577
|
}
|
|
356
578
|
const flushOnHide = () => {
|
|
357
579
|
flushNavTiming();
|
|
358
|
-
if (groups.vitals) {
|
|
580
|
+
if (groups.vitals && shouldEmit()) {
|
|
359
581
|
if (lcp !== null) buffer.pushMetric("__auto_lcp", userId, anonId, { value: lcp });
|
|
360
582
|
if (inp !== null) buffer.pushMetric("__auto_inp", userId, anonId, { value: inp });
|
|
361
583
|
if (clsBad) buffer.pushMetric("__auto_cls_binary", userId, anonId, { value: 1 });
|
|
@@ -438,12 +660,14 @@ var FlagsClientBrowser = class {
|
|
|
438
660
|
baseUrl;
|
|
439
661
|
autoGuardrails;
|
|
440
662
|
autoGuardrailGroups;
|
|
663
|
+
autoCollectAlways;
|
|
441
664
|
env;
|
|
442
665
|
evalResult = null;
|
|
443
666
|
anonId;
|
|
444
667
|
userId = "";
|
|
445
668
|
buffer;
|
|
446
669
|
telemetry;
|
|
670
|
+
seeLimiter = new SeeLimiter();
|
|
447
671
|
guardrailsInstalled = false;
|
|
448
672
|
listeners = /* @__PURE__ */ new Set();
|
|
449
673
|
overrideListenerInstalled = false;
|
|
@@ -459,6 +683,7 @@ var FlagsClientBrowser = class {
|
|
|
459
683
|
this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
|
|
460
684
|
this.env = opts.env ?? "prod";
|
|
461
685
|
this.autoGuardrails = opts.autoGuardrails !== false;
|
|
686
|
+
this.autoCollectAlways = opts.autoCollectAlways === true;
|
|
462
687
|
const g = opts.autoGuardrailGroups ?? {};
|
|
463
688
|
this.autoGuardrailGroups = {
|
|
464
689
|
vitals: g.vitals ?? this.autoGuardrails,
|
|
@@ -503,10 +728,38 @@ var FlagsClientBrowser = class {
|
|
|
503
728
|
const anyGroupOn = this.autoGuardrailGroups.vitals || this.autoGuardrailGroups.errors || this.autoGuardrailGroups.engagement;
|
|
504
729
|
if (anyGroupOn && !this.guardrailsInstalled) {
|
|
505
730
|
this.guardrailsInstalled = true;
|
|
506
|
-
installAutoGuardrails(
|
|
731
|
+
installAutoGuardrails(
|
|
732
|
+
this.buffer,
|
|
733
|
+
this.userId,
|
|
734
|
+
this.anonId,
|
|
735
|
+
this.autoGuardrailGroups,
|
|
736
|
+
(problem, consequence, extras, kind) => this.reportError(problem, consequence, extras, kind),
|
|
737
|
+
[`${this.baseUrl}/`, DEFAULT_TELEMETRY_URL],
|
|
738
|
+
this.autoCollectAlways
|
|
739
|
+
);
|
|
507
740
|
}
|
|
508
741
|
this.notify();
|
|
509
742
|
}
|
|
743
|
+
/**
|
|
744
|
+
* Report a structured error into the errors primitive. Flushes immediately
|
|
745
|
+
* (beacon-first) — error occurrences are near-real-time, never queued behind
|
|
746
|
+
* the 5s metric batch. Spam-guarded by a 30s dedup window + per-session cap.
|
|
747
|
+
*/
|
|
748
|
+
reportError(problem, consequence, extras, kind) {
|
|
749
|
+
try {
|
|
750
|
+
const ev = buildSeeEvent(problem, consequence, extras, {
|
|
751
|
+
side: "client",
|
|
752
|
+
sdkVersion: version,
|
|
753
|
+
env: this.env,
|
|
754
|
+
url: typeof window !== "undefined" && window.location ? window.location.href : void 0,
|
|
755
|
+
userId: this.userId || void 0,
|
|
756
|
+
anonId: this.anonId
|
|
757
|
+
}, kind);
|
|
758
|
+
if (!this.seeLimiter.shouldSend(ev)) return;
|
|
759
|
+
this.buffer.sendNow([ev]);
|
|
760
|
+
} catch {
|
|
761
|
+
}
|
|
762
|
+
}
|
|
510
763
|
get ready() {
|
|
511
764
|
return this.evalResult !== null;
|
|
512
765
|
}
|
|
@@ -738,13 +991,15 @@ var _client = null;
|
|
|
738
991
|
function shipeasy(opts) {
|
|
739
992
|
const ac = opts.autoCollect;
|
|
740
993
|
const blanket = ac === false ? false : true;
|
|
741
|
-
const
|
|
994
|
+
const acObj = ac && typeof ac === "object" ? ac : void 0;
|
|
995
|
+
const groups = acObj ? { vitals: acObj.vitals, errors: acObj.errors, engagement: acObj.engagement } : void 0;
|
|
742
996
|
const baseUrl = opts.baseUrl ?? "https://cdn.shipeasy.ai";
|
|
743
997
|
const client = configureShipeasy({
|
|
744
998
|
sdkKey: opts.clientKey,
|
|
745
999
|
baseUrl,
|
|
746
1000
|
autoGuardrails: blanket,
|
|
747
1001
|
autoGuardrailGroups: groups,
|
|
1002
|
+
autoCollectAlways: acObj?.always === true,
|
|
748
1003
|
disableTelemetry: opts.disableTelemetry
|
|
749
1004
|
});
|
|
750
1005
|
injectI18nLoader(opts.clientKey, baseUrl, opts.i18nProfile);
|
|
@@ -906,6 +1161,20 @@ var flags = {
|
|
|
906
1161
|
return _client?.ready ?? false;
|
|
907
1162
|
}
|
|
908
1163
|
};
|
|
1164
|
+
function dispatchSee(problem, consequence, extras, kind) {
|
|
1165
|
+
if (!_client) {
|
|
1166
|
+
console.warn("[shipeasy] see() called before shipeasy({ clientKey }) \u2014 error dropped");
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
_client.reportError(problem, consequence, extras, kind);
|
|
1170
|
+
}
|
|
1171
|
+
var see = Object.assign(
|
|
1172
|
+
(problem) => startSeeChain(() => problem, dispatchSee),
|
|
1173
|
+
{
|
|
1174
|
+
Violation: (name) => startSeeViolationChain(name, dispatchSee),
|
|
1175
|
+
ControlFlowException: markExpected
|
|
1176
|
+
}
|
|
1177
|
+
);
|
|
909
1178
|
var LABEL_MARKER_START = "\uFFF9";
|
|
910
1179
|
var LABEL_MARKER_SEP = "\uFFFA";
|
|
911
1180
|
var LABEL_MARKER_END = "\uFFFB";
|
|
@@ -1157,6 +1426,7 @@ export {
|
|
|
1157
1426
|
readConfigOverride,
|
|
1158
1427
|
readExpOverride,
|
|
1159
1428
|
readGateOverride,
|
|
1429
|
+
see,
|
|
1160
1430
|
shipeasy,
|
|
1161
1431
|
version
|
|
1162
1432
|
};
|
package/dist/server/index.d.mts
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 };
|