@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/client/index.js
CHANGED
|
@@ -38,11 +38,273 @@ __export(client_exports, {
|
|
|
38
38
|
readConfigOverride: () => readConfigOverride,
|
|
39
39
|
readExpOverride: () => readExpOverride,
|
|
40
40
|
readGateOverride: () => readGateOverride,
|
|
41
|
+
see: () => see,
|
|
41
42
|
shipeasy: () => shipeasy,
|
|
42
43
|
version: () => version
|
|
43
44
|
});
|
|
44
45
|
module.exports = __toCommonJS(client_exports);
|
|
45
|
-
|
|
46
|
+
|
|
47
|
+
// src/telemetry.ts
|
|
48
|
+
async function sha256Hex(input) {
|
|
49
|
+
const buf = new TextEncoder().encode(input);
|
|
50
|
+
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
51
|
+
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
52
|
+
}
|
|
53
|
+
var DEFAULT_TELEMETRY_URL = "https://t.shipeasy.ai";
|
|
54
|
+
var Telemetry = class {
|
|
55
|
+
prefix;
|
|
56
|
+
disabled;
|
|
57
|
+
dedupeMs;
|
|
58
|
+
// Last-emit timestamp per `feature/resource`, for the dedup window. Bounded by
|
|
59
|
+
// the number of distinct keys the app reads.
|
|
60
|
+
lastEmit = /* @__PURE__ */ new Map();
|
|
61
|
+
// Resolved once at construction and reused by every emit(), so the per-eval
|
|
62
|
+
// cost is a Map-free microtask, not a hash.
|
|
63
|
+
keyHash;
|
|
64
|
+
constructor(opts) {
|
|
65
|
+
const endpoint = (opts.endpoint ?? "").replace(/\/$/, "");
|
|
66
|
+
this.disabled = opts.disabled === true || !opts.sdkKey || !endpoint;
|
|
67
|
+
this.dedupeMs = opts.dedupeMs ?? 2e3;
|
|
68
|
+
this.prefix = `${endpoint}/t`;
|
|
69
|
+
this.keyHash = this.disabled ? null : sha256Hex(opts.sdkKey).then((h) => `${h}/${opts.side}/${encodeURIComponent(opts.env)}`).catch(() => "");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Emit a single best-effort usage beacon for one evaluation. Never blocks the
|
|
73
|
+
* caller (the hash is already resolved) and never throws — a failed beacon
|
|
74
|
+
* must never affect the evaluation it measures.
|
|
75
|
+
*/
|
|
76
|
+
emit(feature, resource) {
|
|
77
|
+
if (this.disabled || !this.keyHash) return;
|
|
78
|
+
if (this.dedupeMs > 0) {
|
|
79
|
+
const dedupeKey = `${feature}/${resource}`;
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
const last = this.lastEmit.get(dedupeKey);
|
|
82
|
+
if (last !== void 0 && now - last < this.dedupeMs) return;
|
|
83
|
+
this.lastEmit.set(dedupeKey, now);
|
|
84
|
+
}
|
|
85
|
+
void this.keyHash.then((suffix) => {
|
|
86
|
+
if (!suffix) return;
|
|
87
|
+
send(`${this.prefix}/${suffix}/${feature}/${encodeURIComponent(resource)}`);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
function send(url) {
|
|
92
|
+
try {
|
|
93
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
94
|
+
navigator.sendBeacon(url);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const f = globalThis.fetch;
|
|
98
|
+
if (typeof f === "function") {
|
|
99
|
+
void f(url, { method: "GET", keepalive: true }).catch(() => {
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/see/core.ts
|
|
107
|
+
var SEE_MAX_MESSAGE = 500;
|
|
108
|
+
var SEE_MAX_STACK = 8e3;
|
|
109
|
+
var SEE_MAX_SUBJECT = 200;
|
|
110
|
+
var SEE_MAX_EXTRA_VALUE = 200;
|
|
111
|
+
var SEE_MAX_EXTRA_KEYS = 20;
|
|
112
|
+
var SEE_DEDUP_WINDOW_MS = 3e4;
|
|
113
|
+
var SEE_MAX_PER_SESSION = 25;
|
|
114
|
+
function causesThe(subject) {
|
|
115
|
+
return {
|
|
116
|
+
to(outcome) {
|
|
117
|
+
return {
|
|
118
|
+
__seConsequence: true,
|
|
119
|
+
subject: truncate(String(subject), SEE_MAX_SUBJECT),
|
|
120
|
+
outcome: truncate(String(outcome), SEE_MAX_SUBJECT)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function violation(name) {
|
|
126
|
+
const make = (msg) => ({
|
|
127
|
+
__seViolation: true,
|
|
128
|
+
violationName: String(name),
|
|
129
|
+
...msg !== void 0 ? { violationMessage: msg } : {},
|
|
130
|
+
message(m) {
|
|
131
|
+
return make(String(m));
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
return make();
|
|
135
|
+
}
|
|
136
|
+
function isViolation(p) {
|
|
137
|
+
return typeof p === "object" && p !== null && p.__seViolation === true;
|
|
138
|
+
}
|
|
139
|
+
var EXPECTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-expected");
|
|
140
|
+
function markExpected(err, because) {
|
|
141
|
+
if (typeof err !== "object" || err === null) return;
|
|
142
|
+
try {
|
|
143
|
+
Object.defineProperty(err, EXPECTED_SYM, {
|
|
144
|
+
value: String(because),
|
|
145
|
+
enumerable: false,
|
|
146
|
+
configurable: true
|
|
147
|
+
});
|
|
148
|
+
} catch {
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function isExpected(err) {
|
|
152
|
+
if (typeof err !== "object" || err === null) return false;
|
|
153
|
+
return err[EXPECTED_SYM] !== void 0;
|
|
154
|
+
}
|
|
155
|
+
function truncate(s, max) {
|
|
156
|
+
return s.length > max ? s.slice(0, max) : s;
|
|
157
|
+
}
|
|
158
|
+
function sanitizeExtras(extras) {
|
|
159
|
+
if (!extras || typeof extras !== "object") return void 0;
|
|
160
|
+
const out = {};
|
|
161
|
+
let n = 0;
|
|
162
|
+
for (const [k, v] of Object.entries(extras)) {
|
|
163
|
+
if (v === null || v === void 0) continue;
|
|
164
|
+
if (n >= SEE_MAX_EXTRA_KEYS) break;
|
|
165
|
+
if (typeof v === "string") out[k] = truncate(v, SEE_MAX_EXTRA_VALUE);
|
|
166
|
+
else if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
|
|
167
|
+
else if (typeof v === "boolean") out[k] = v;
|
|
168
|
+
else continue;
|
|
169
|
+
n += 1;
|
|
170
|
+
}
|
|
171
|
+
return n > 0 ? out : void 0;
|
|
172
|
+
}
|
|
173
|
+
function captureCallsiteStack() {
|
|
174
|
+
const raw = new Error().stack;
|
|
175
|
+
if (!raw) return void 0;
|
|
176
|
+
const lines = raw.split("\n");
|
|
177
|
+
const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
|
|
178
|
+
return kept.length ? kept.join("\n") : void 0;
|
|
179
|
+
}
|
|
180
|
+
function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
|
|
181
|
+
let errorType;
|
|
182
|
+
let message;
|
|
183
|
+
let stack;
|
|
184
|
+
let kind;
|
|
185
|
+
if (isViolation(problem)) {
|
|
186
|
+
errorType = problem.violationName;
|
|
187
|
+
message = problem.violationMessage ?? problem.violationName;
|
|
188
|
+
stack = captureCallsiteStack();
|
|
189
|
+
kind = kindOverride ?? "violation";
|
|
190
|
+
} else if (problem instanceof Error) {
|
|
191
|
+
errorType = problem.name || "Error";
|
|
192
|
+
message = problem.message || String(problem);
|
|
193
|
+
stack = problem.stack ?? void 0;
|
|
194
|
+
kind = kindOverride ?? "caught";
|
|
195
|
+
} else {
|
|
196
|
+
errorType = "Error";
|
|
197
|
+
message = typeof problem === "string" ? problem : safeString(problem);
|
|
198
|
+
stack = captureCallsiteStack();
|
|
199
|
+
kind = kindOverride ?? "caught";
|
|
200
|
+
}
|
|
201
|
+
const ev = {
|
|
202
|
+
type: "error",
|
|
203
|
+
kind,
|
|
204
|
+
error_type: truncate(errorType, SEE_MAX_SUBJECT),
|
|
205
|
+
message: truncate(message, SEE_MAX_MESSAGE),
|
|
206
|
+
subject: consequence.subject,
|
|
207
|
+
outcome: consequence.outcome,
|
|
208
|
+
side: ctx.side,
|
|
209
|
+
sdk_version: ctx.sdkVersion,
|
|
210
|
+
ts: Date.now()
|
|
211
|
+
};
|
|
212
|
+
if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
|
|
213
|
+
const cleanExtras = sanitizeExtras(extras);
|
|
214
|
+
if (cleanExtras) ev.extras = cleanExtras;
|
|
215
|
+
if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
|
|
216
|
+
if (ctx.userId) ev.user_id = ctx.userId;
|
|
217
|
+
if (ctx.anonId) ev.anonymous_id = ctx.anonId;
|
|
218
|
+
if (ctx.env) ev.env = ctx.env;
|
|
219
|
+
return ev;
|
|
220
|
+
}
|
|
221
|
+
function safeString(v) {
|
|
222
|
+
try {
|
|
223
|
+
return typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
224
|
+
} catch {
|
|
225
|
+
return String(v);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
var scheduleMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : (cb) => {
|
|
229
|
+
void Promise.resolve().then(cb);
|
|
230
|
+
};
|
|
231
|
+
function startSeeChain(getProblem, dispatch) {
|
|
232
|
+
let subject;
|
|
233
|
+
let outcome;
|
|
234
|
+
let collected;
|
|
235
|
+
let flushed = false;
|
|
236
|
+
scheduleMicrotask(() => {
|
|
237
|
+
if (flushed) return;
|
|
238
|
+
flushed = true;
|
|
239
|
+
dispatch(
|
|
240
|
+
getProblem(),
|
|
241
|
+
causesThe(subject ?? "the app").to(outcome ?? "hit an error"),
|
|
242
|
+
collected
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
const tail = {
|
|
246
|
+
extras(x) {
|
|
247
|
+
if (x && typeof x === "object") collected = { ...collected, ...x };
|
|
248
|
+
return tail;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
const step = {
|
|
252
|
+
to(o) {
|
|
253
|
+
outcome = String(o);
|
|
254
|
+
return tail;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
const start = (s) => {
|
|
258
|
+
subject = String(s);
|
|
259
|
+
return step;
|
|
260
|
+
};
|
|
261
|
+
return { causes_the: start, causesThe: start };
|
|
262
|
+
}
|
|
263
|
+
function startSeeViolationChain(name, dispatch) {
|
|
264
|
+
let msg;
|
|
265
|
+
const base = startSeeChain(
|
|
266
|
+
() => msg !== void 0 ? violation(name).message(msg) : violation(name),
|
|
267
|
+
dispatch
|
|
268
|
+
);
|
|
269
|
+
const chain = {
|
|
270
|
+
...base,
|
|
271
|
+
message(m) {
|
|
272
|
+
msg = String(m);
|
|
273
|
+
return chain;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
return chain;
|
|
277
|
+
}
|
|
278
|
+
function topStackLine(stack) {
|
|
279
|
+
if (!stack) return "";
|
|
280
|
+
for (const line of stack.split("\n")) {
|
|
281
|
+
if (/^\s*at |@|:\d+:\d+/.test(line)) return line.trim().slice(0, 200);
|
|
282
|
+
}
|
|
283
|
+
return "";
|
|
284
|
+
}
|
|
285
|
+
var SeeLimiter = class {
|
|
286
|
+
constructor(maxPerSession = SEE_MAX_PER_SESSION, dedupWindowMs = SEE_DEDUP_WINDOW_MS) {
|
|
287
|
+
this.maxPerSession = maxPerSession;
|
|
288
|
+
this.dedupWindowMs = dedupWindowMs;
|
|
289
|
+
}
|
|
290
|
+
maxPerSession;
|
|
291
|
+
dedupWindowMs;
|
|
292
|
+
lastSent = /* @__PURE__ */ new Map();
|
|
293
|
+
sent = 0;
|
|
294
|
+
shouldSend(ev) {
|
|
295
|
+
if (this.sent >= this.maxPerSession) return false;
|
|
296
|
+
const key = `${ev.kind}|${ev.error_type}|${ev.message.slice(0, 200)}|${topStackLine(ev.stack)}`;
|
|
297
|
+
const now = Date.now();
|
|
298
|
+
const prev = this.lastSent.get(key);
|
|
299
|
+
if (prev !== void 0 && now - prev < this.dedupWindowMs) return false;
|
|
300
|
+
this.lastSent.set(key, now);
|
|
301
|
+
this.sent += 1;
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// src/client/index.ts
|
|
307
|
+
var version = "4.0.0";
|
|
46
308
|
var FLUSH_INTERVAL_MS = 5e3;
|
|
47
309
|
var MAX_BUFFER = 100;
|
|
48
310
|
var ANON_ID_KEY = "__se_anon_id";
|
|
@@ -76,6 +338,13 @@ var EventBuffer = class {
|
|
|
76
338
|
this.timer = null;
|
|
77
339
|
}
|
|
78
340
|
}
|
|
341
|
+
/** True once this visitor has been exposed to ≥1 experiment (this tab or a
|
|
342
|
+
* prior page in the session — the dedup set persists in sessionStorage).
|
|
343
|
+
* Gates auto-metric emission: vitals from non-participants are never read
|
|
344
|
+
* by the analysis pipeline and would be pure AE write cost (see cost.md). */
|
|
345
|
+
hasExposures() {
|
|
346
|
+
return this.exposureSeen.size > 0;
|
|
347
|
+
}
|
|
79
348
|
pushExposure(experiment, group, userId, anonId) {
|
|
80
349
|
const key = `${userId || anonId}:${experiment}`;
|
|
81
350
|
if (this.exposureSeen.has(key)) return;
|
|
@@ -141,16 +410,29 @@ var EventBuffer = class {
|
|
|
141
410
|
flush(useBeacon = false) {
|
|
142
411
|
if (!this.queue.length) return;
|
|
143
412
|
const batch = this.queue.splice(0);
|
|
144
|
-
|
|
413
|
+
this.send(batch, useBeacon);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Bypass the 5s queue and ship events immediately — used by see() error
|
|
417
|
+
* reporting so occurrences land near-real-time and survive page unload.
|
|
418
|
+
* Beacon-first (fire-and-forget, unload-safe), keepalive fetch fallback.
|
|
419
|
+
*/
|
|
420
|
+
sendNow(events) {
|
|
421
|
+
this.send(events, true);
|
|
422
|
+
}
|
|
423
|
+
send(batch, useBeacon) {
|
|
145
424
|
if (useBeacon && typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
146
425
|
const beaconBody = JSON.stringify({ k: this.sdkKey, events: batch });
|
|
147
|
-
|
|
148
|
-
|
|
426
|
+
try {
|
|
427
|
+
if (navigator.sendBeacon(this.collectUrl, new Blob([beaconBody], { type: "text/plain" })))
|
|
428
|
+
return;
|
|
429
|
+
} catch {
|
|
430
|
+
}
|
|
149
431
|
}
|
|
150
432
|
fetch(this.collectUrl, {
|
|
151
433
|
method: "POST",
|
|
152
434
|
headers: { "X-SDK-Key": this.sdkKey, "Content-Type": "application/json" },
|
|
153
|
-
body,
|
|
435
|
+
body: JSON.stringify({ events: batch }),
|
|
154
436
|
keepalive: true
|
|
155
437
|
}).catch(() => {
|
|
156
438
|
});
|
|
@@ -167,14 +449,12 @@ var EventBuffer = class {
|
|
|
167
449
|
});
|
|
168
450
|
}
|
|
169
451
|
};
|
|
170
|
-
|
|
171
|
-
function installAutoGuardrails(buffer, userId, anonId, groups) {
|
|
452
|
+
function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignoreUrlPrefixes, always = false) {
|
|
172
453
|
if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") return;
|
|
454
|
+
const shouldEmit = () => always || buffer.hasExposures();
|
|
173
455
|
let lcp = null;
|
|
174
456
|
let inp = null;
|
|
175
457
|
let clsBad = false;
|
|
176
|
-
let jsErrorCount = 0;
|
|
177
|
-
let netErrorCount = 0;
|
|
178
458
|
let navTimingFlushed = false;
|
|
179
459
|
if (groups.vitals) {
|
|
180
460
|
try {
|
|
@@ -213,68 +493,71 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
|
|
|
213
493
|
if (groups.errors) {
|
|
214
494
|
const origOnError = window.onerror;
|
|
215
495
|
window.onerror = (msg, source, lineno, _colno, err) => {
|
|
216
|
-
if (
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
496
|
+
if (!isExpected(err)) {
|
|
497
|
+
const problem = err ?? (typeof msg === "string" && msg ? msg : "Unknown error");
|
|
498
|
+
reportSee(
|
|
499
|
+
problem,
|
|
500
|
+
causesThe("the page").to("hit an unhandled error"),
|
|
501
|
+
{
|
|
502
|
+
source: typeof source === "string" ? source : void 0,
|
|
503
|
+
line: lineno ?? void 0
|
|
504
|
+
},
|
|
505
|
+
"uncaught"
|
|
506
|
+
);
|
|
225
507
|
}
|
|
226
508
|
if (typeof origOnError === "function") return origOnError(msg, source, lineno, _colno, err);
|
|
227
509
|
return false;
|
|
228
510
|
};
|
|
229
511
|
window.addEventListener("unhandledrejection", (e) => {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
});
|
|
239
|
-
}
|
|
512
|
+
const reason = e.reason;
|
|
513
|
+
if (isExpected(reason)) return;
|
|
514
|
+
reportSee(
|
|
515
|
+
reason ?? "Unhandled promise rejection",
|
|
516
|
+
causesThe("the page").to("hit an unhandled promise rejection"),
|
|
517
|
+
void 0,
|
|
518
|
+
"unhandled_rejection"
|
|
519
|
+
);
|
|
240
520
|
});
|
|
241
521
|
const origFetch = window.fetch;
|
|
242
522
|
window.fetch = async function(...args) {
|
|
243
523
|
const startedAt = typeof performance !== "undefined" ? performance.now() : 0;
|
|
244
524
|
const url = typeof args[0] === "string" ? args[0] : args[0].toString();
|
|
525
|
+
const ignored = ignoreUrlPrefixes.some((p) => p && url.startsWith(p));
|
|
526
|
+
const bareUrl = url.split("?")[0].slice(0, 200);
|
|
245
527
|
let res;
|
|
246
528
|
try {
|
|
247
529
|
res = await origFetch.apply(this, args);
|
|
248
530
|
} catch (err) {
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
});
|
|
531
|
+
if (!ignored && !isExpected(err)) {
|
|
532
|
+
reportSee(
|
|
533
|
+
violation("NetworkError").message(`request to ${bareUrl} failed`),
|
|
534
|
+
causesThe("a network request").to("fail without a response"),
|
|
535
|
+
{ status: 0, url: url.slice(0, 200) },
|
|
536
|
+
"network"
|
|
537
|
+
);
|
|
257
538
|
}
|
|
258
539
|
throw err;
|
|
259
540
|
}
|
|
260
|
-
if (res.status >= 500
|
|
261
|
-
netErrorCount += 1;
|
|
541
|
+
if (!ignored && res.status >= 500) {
|
|
262
542
|
const elapsed = typeof performance !== "undefined" ? performance.now() - startedAt : 0;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
status: res.status,
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
});
|
|
543
|
+
reportSee(
|
|
544
|
+
violation("Http5xx").message(`request to ${bareUrl} returned ${res.status}`),
|
|
545
|
+
causesThe("a network request").to(`fail with HTTP ${res.status}`),
|
|
546
|
+
{ status: res.status, url: url.slice(0, 200), duration_ms: Math.round(elapsed) },
|
|
547
|
+
"network"
|
|
548
|
+
);
|
|
270
549
|
}
|
|
271
550
|
return res;
|
|
272
551
|
};
|
|
273
552
|
}
|
|
274
553
|
const flushNavTiming = () => {
|
|
275
554
|
if (navTimingFlushed) return;
|
|
555
|
+
if (!groups.vitals) {
|
|
556
|
+
navTimingFlushed = true;
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (!shouldEmit()) return;
|
|
276
560
|
navTimingFlushed = true;
|
|
277
|
-
if (!groups.vitals) return;
|
|
278
561
|
try {
|
|
279
562
|
const navList = performance.getEntriesByType("navigation");
|
|
280
563
|
const nav = navList[0];
|
|
@@ -309,7 +592,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
|
|
|
309
592
|
};
|
|
310
593
|
if (groups.engagement) {
|
|
311
594
|
try {
|
|
312
|
-
buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
|
|
595
|
+
if (shouldEmit()) buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
|
|
313
596
|
} catch {
|
|
314
597
|
}
|
|
315
598
|
let lastEmit = Date.now();
|
|
@@ -317,6 +600,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
|
|
|
317
600
|
document.addEventListener("visibilitychange", () => {
|
|
318
601
|
if (document.visibilityState !== "visible") return;
|
|
319
602
|
if (Date.now() - lastEmit < SESSION_GAP_MS) return;
|
|
603
|
+
if (!shouldEmit()) return;
|
|
320
604
|
try {
|
|
321
605
|
buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
|
|
322
606
|
lastEmit = Date.now();
|
|
@@ -339,7 +623,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
|
|
|
339
623
|
}
|
|
340
624
|
const flushOnHide = () => {
|
|
341
625
|
flushNavTiming();
|
|
342
|
-
if (groups.vitals) {
|
|
626
|
+
if (groups.vitals && shouldEmit()) {
|
|
343
627
|
if (lcp !== null) buffer.pushMetric("__auto_lcp", userId, anonId, { value: lcp });
|
|
344
628
|
if (inp !== null) buffer.pushMetric("__auto_inp", userId, anonId, { value: inp });
|
|
345
629
|
if (clsBad) buffer.pushMetric("__auto_cls_binary", userId, anonId, { value: 1 });
|
|
@@ -422,11 +706,14 @@ var FlagsClientBrowser = class {
|
|
|
422
706
|
baseUrl;
|
|
423
707
|
autoGuardrails;
|
|
424
708
|
autoGuardrailGroups;
|
|
709
|
+
autoCollectAlways;
|
|
425
710
|
env;
|
|
426
711
|
evalResult = null;
|
|
427
712
|
anonId;
|
|
428
713
|
userId = "";
|
|
429
714
|
buffer;
|
|
715
|
+
telemetry;
|
|
716
|
+
seeLimiter = new SeeLimiter();
|
|
430
717
|
guardrailsInstalled = false;
|
|
431
718
|
listeners = /* @__PURE__ */ new Set();
|
|
432
719
|
overrideListenerInstalled = false;
|
|
@@ -442,6 +729,7 @@ var FlagsClientBrowser = class {
|
|
|
442
729
|
this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
|
|
443
730
|
this.env = opts.env ?? "prod";
|
|
444
731
|
this.autoGuardrails = opts.autoGuardrails !== false;
|
|
732
|
+
this.autoCollectAlways = opts.autoCollectAlways === true;
|
|
445
733
|
const g = opts.autoGuardrailGroups ?? {};
|
|
446
734
|
this.autoGuardrailGroups = {
|
|
447
735
|
vitals: g.vitals ?? this.autoGuardrails,
|
|
@@ -450,6 +738,13 @@ var FlagsClientBrowser = class {
|
|
|
450
738
|
};
|
|
451
739
|
this.anonId = getOrCreateAnonId();
|
|
452
740
|
this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
|
|
741
|
+
this.telemetry = new Telemetry({
|
|
742
|
+
endpoint: opts.telemetryUrl ?? DEFAULT_TELEMETRY_URL,
|
|
743
|
+
sdkKey: this.sdkKey,
|
|
744
|
+
side: "client",
|
|
745
|
+
env: this.env,
|
|
746
|
+
disabled: opts.disableTelemetry
|
|
747
|
+
});
|
|
453
748
|
void this.buffer.flushPendingAlias();
|
|
454
749
|
}
|
|
455
750
|
async identify(user) {
|
|
@@ -479,10 +774,38 @@ var FlagsClientBrowser = class {
|
|
|
479
774
|
const anyGroupOn = this.autoGuardrailGroups.vitals || this.autoGuardrailGroups.errors || this.autoGuardrailGroups.engagement;
|
|
480
775
|
if (anyGroupOn && !this.guardrailsInstalled) {
|
|
481
776
|
this.guardrailsInstalled = true;
|
|
482
|
-
installAutoGuardrails(
|
|
777
|
+
installAutoGuardrails(
|
|
778
|
+
this.buffer,
|
|
779
|
+
this.userId,
|
|
780
|
+
this.anonId,
|
|
781
|
+
this.autoGuardrailGroups,
|
|
782
|
+
(problem, consequence, extras, kind) => this.reportError(problem, consequence, extras, kind),
|
|
783
|
+
[`${this.baseUrl}/`, DEFAULT_TELEMETRY_URL],
|
|
784
|
+
this.autoCollectAlways
|
|
785
|
+
);
|
|
483
786
|
}
|
|
484
787
|
this.notify();
|
|
485
788
|
}
|
|
789
|
+
/**
|
|
790
|
+
* Report a structured error into the errors primitive. Flushes immediately
|
|
791
|
+
* (beacon-first) — error occurrences are near-real-time, never queued behind
|
|
792
|
+
* the 5s metric batch. Spam-guarded by a 30s dedup window + per-session cap.
|
|
793
|
+
*/
|
|
794
|
+
reportError(problem, consequence, extras, kind) {
|
|
795
|
+
try {
|
|
796
|
+
const ev = buildSeeEvent(problem, consequence, extras, {
|
|
797
|
+
side: "client",
|
|
798
|
+
sdkVersion: version,
|
|
799
|
+
env: this.env,
|
|
800
|
+
url: typeof window !== "undefined" && window.location ? window.location.href : void 0,
|
|
801
|
+
userId: this.userId || void 0,
|
|
802
|
+
anonId: this.anonId
|
|
803
|
+
}, kind);
|
|
804
|
+
if (!this.seeLimiter.shouldSend(ev)) return;
|
|
805
|
+
this.buffer.sendNow([ev]);
|
|
806
|
+
} catch {
|
|
807
|
+
}
|
|
808
|
+
}
|
|
486
809
|
get ready() {
|
|
487
810
|
return this.evalResult !== null;
|
|
488
811
|
}
|
|
@@ -499,12 +822,14 @@ var FlagsClientBrowser = class {
|
|
|
499
822
|
this.evalResult = data;
|
|
500
823
|
}
|
|
501
824
|
getFlag(name) {
|
|
825
|
+
this.telemetry.emit("gate", name);
|
|
502
826
|
if (this.evalResult === null) return false;
|
|
503
827
|
const ov = readGateOverride(name);
|
|
504
828
|
if (ov !== null) return ov;
|
|
505
829
|
return this.evalResult.flags[name] ?? false;
|
|
506
830
|
}
|
|
507
831
|
getConfig(name, decode) {
|
|
832
|
+
this.telemetry.emit("config", name);
|
|
508
833
|
if (this.evalResult === null) return void 0;
|
|
509
834
|
const ov = readConfigOverride(name);
|
|
510
835
|
const raw = ov !== void 0 ? ov : this.evalResult.configs?.[name];
|
|
@@ -518,6 +843,7 @@ var FlagsClientBrowser = class {
|
|
|
518
843
|
}
|
|
519
844
|
}
|
|
520
845
|
getExperiment(name, defaultParams, decode, variants) {
|
|
846
|
+
this.telemetry.emit("experiment", name);
|
|
521
847
|
const notIn = {
|
|
522
848
|
inExperiment: false,
|
|
523
849
|
group: "control",
|
|
@@ -582,6 +908,7 @@ var FlagsClientBrowser = class {
|
|
|
582
908
|
* the per-switch state. Returns false for unknown killswitches / switches.
|
|
583
909
|
*/
|
|
584
910
|
getKillswitch(name, switchKey) {
|
|
911
|
+
this.telemetry.emit("ks", name);
|
|
585
912
|
if (this.evalResult === null) return false;
|
|
586
913
|
const ks = this.evalResult.killswitches?.[name];
|
|
587
914
|
if (ks === void 0) return false;
|
|
@@ -710,13 +1037,16 @@ var _client = null;
|
|
|
710
1037
|
function shipeasy(opts) {
|
|
711
1038
|
const ac = opts.autoCollect;
|
|
712
1039
|
const blanket = ac === false ? false : true;
|
|
713
|
-
const
|
|
1040
|
+
const acObj = ac && typeof ac === "object" ? ac : void 0;
|
|
1041
|
+
const groups = acObj ? { vitals: acObj.vitals, errors: acObj.errors, engagement: acObj.engagement } : void 0;
|
|
714
1042
|
const baseUrl = opts.baseUrl ?? "https://cdn.shipeasy.ai";
|
|
715
1043
|
const client = configureShipeasy({
|
|
716
1044
|
sdkKey: opts.clientKey,
|
|
717
1045
|
baseUrl,
|
|
718
1046
|
autoGuardrails: blanket,
|
|
719
|
-
autoGuardrailGroups: groups
|
|
1047
|
+
autoGuardrailGroups: groups,
|
|
1048
|
+
autoCollectAlways: acObj?.always === true,
|
|
1049
|
+
disableTelemetry: opts.disableTelemetry
|
|
720
1050
|
});
|
|
721
1051
|
injectI18nLoader(opts.clientKey, baseUrl, opts.i18nProfile);
|
|
722
1052
|
flags.notifyMounted();
|
|
@@ -877,6 +1207,20 @@ var flags = {
|
|
|
877
1207
|
return _client?.ready ?? false;
|
|
878
1208
|
}
|
|
879
1209
|
};
|
|
1210
|
+
function dispatchSee(problem, consequence, extras, kind) {
|
|
1211
|
+
if (!_client) {
|
|
1212
|
+
console.warn("[shipeasy] see() called before shipeasy({ clientKey }) \u2014 error dropped");
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
_client.reportError(problem, consequence, extras, kind);
|
|
1216
|
+
}
|
|
1217
|
+
var see = Object.assign(
|
|
1218
|
+
(problem) => startSeeChain(() => problem, dispatchSee),
|
|
1219
|
+
{
|
|
1220
|
+
Violation: (name) => startSeeViolationChain(name, dispatchSee),
|
|
1221
|
+
ControlFlowException: markExpected
|
|
1222
|
+
}
|
|
1223
|
+
);
|
|
880
1224
|
var LABEL_MARKER_START = "\uFFF9";
|
|
881
1225
|
var LABEL_MARKER_SEP = "\uFFFA";
|
|
882
1226
|
var LABEL_MARKER_END = "\uFFFB";
|
|
@@ -1129,6 +1473,7 @@ var i18n = {
|
|
|
1129
1473
|
readConfigOverride,
|
|
1130
1474
|
readExpOverride,
|
|
1131
1475
|
readGateOverride,
|
|
1476
|
+
see,
|
|
1132
1477
|
shipeasy,
|
|
1133
1478
|
version
|
|
1134
1479
|
});
|