@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.mjs
CHANGED
|
@@ -1,5 +1,264 @@
|
|
|
1
|
+
// src/telemetry.ts
|
|
2
|
+
async function sha256Hex(input) {
|
|
3
|
+
const buf = new TextEncoder().encode(input);
|
|
4
|
+
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
5
|
+
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
6
|
+
}
|
|
7
|
+
var DEFAULT_TELEMETRY_URL = "https://t.shipeasy.ai";
|
|
8
|
+
var Telemetry = class {
|
|
9
|
+
prefix;
|
|
10
|
+
disabled;
|
|
11
|
+
dedupeMs;
|
|
12
|
+
// Last-emit timestamp per `feature/resource`, for the dedup window. Bounded by
|
|
13
|
+
// the number of distinct keys the app reads.
|
|
14
|
+
lastEmit = /* @__PURE__ */ new Map();
|
|
15
|
+
// Resolved once at construction and reused by every emit(), so the per-eval
|
|
16
|
+
// cost is a Map-free microtask, not a hash.
|
|
17
|
+
keyHash;
|
|
18
|
+
constructor(opts) {
|
|
19
|
+
const endpoint = (opts.endpoint ?? "").replace(/\/$/, "");
|
|
20
|
+
this.disabled = opts.disabled === true || !opts.sdkKey || !endpoint;
|
|
21
|
+
this.dedupeMs = opts.dedupeMs ?? 2e3;
|
|
22
|
+
this.prefix = `${endpoint}/t`;
|
|
23
|
+
this.keyHash = this.disabled ? null : sha256Hex(opts.sdkKey).then((h) => `${h}/${opts.side}/${encodeURIComponent(opts.env)}`).catch(() => "");
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Emit a single best-effort usage beacon for one evaluation. Never blocks the
|
|
27
|
+
* caller (the hash is already resolved) and never throws — a failed beacon
|
|
28
|
+
* must never affect the evaluation it measures.
|
|
29
|
+
*/
|
|
30
|
+
emit(feature, resource) {
|
|
31
|
+
if (this.disabled || !this.keyHash) return;
|
|
32
|
+
if (this.dedupeMs > 0) {
|
|
33
|
+
const dedupeKey = `${feature}/${resource}`;
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
const last = this.lastEmit.get(dedupeKey);
|
|
36
|
+
if (last !== void 0 && now - last < this.dedupeMs) return;
|
|
37
|
+
this.lastEmit.set(dedupeKey, now);
|
|
38
|
+
}
|
|
39
|
+
void this.keyHash.then((suffix) => {
|
|
40
|
+
if (!suffix) return;
|
|
41
|
+
send(`${this.prefix}/${suffix}/${feature}/${encodeURIComponent(resource)}`);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function send(url) {
|
|
46
|
+
try {
|
|
47
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
48
|
+
navigator.sendBeacon(url);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const f = globalThis.fetch;
|
|
52
|
+
if (typeof f === "function") {
|
|
53
|
+
void f(url, { method: "GET", keepalive: true }).catch(() => {
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
}
|
|
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
|
+
|
|
1
260
|
// src/client/index.ts
|
|
2
|
-
var version = "
|
|
261
|
+
var version = "4.0.0";
|
|
3
262
|
var FLUSH_INTERVAL_MS = 5e3;
|
|
4
263
|
var MAX_BUFFER = 100;
|
|
5
264
|
var ANON_ID_KEY = "__se_anon_id";
|
|
@@ -33,6 +292,13 @@ var EventBuffer = class {
|
|
|
33
292
|
this.timer = null;
|
|
34
293
|
}
|
|
35
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
|
+
}
|
|
36
302
|
pushExposure(experiment, group, userId, anonId) {
|
|
37
303
|
const key = `${userId || anonId}:${experiment}`;
|
|
38
304
|
if (this.exposureSeen.has(key)) return;
|
|
@@ -98,16 +364,29 @@ var EventBuffer = class {
|
|
|
98
364
|
flush(useBeacon = false) {
|
|
99
365
|
if (!this.queue.length) return;
|
|
100
366
|
const batch = this.queue.splice(0);
|
|
101
|
-
|
|
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) {
|
|
102
378
|
if (useBeacon && typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
103
379
|
const beaconBody = JSON.stringify({ k: this.sdkKey, events: batch });
|
|
104
|
-
|
|
105
|
-
|
|
380
|
+
try {
|
|
381
|
+
if (navigator.sendBeacon(this.collectUrl, new Blob([beaconBody], { type: "text/plain" })))
|
|
382
|
+
return;
|
|
383
|
+
} catch {
|
|
384
|
+
}
|
|
106
385
|
}
|
|
107
386
|
fetch(this.collectUrl, {
|
|
108
387
|
method: "POST",
|
|
109
388
|
headers: { "X-SDK-Key": this.sdkKey, "Content-Type": "application/json" },
|
|
110
|
-
body,
|
|
389
|
+
body: JSON.stringify({ events: batch }),
|
|
111
390
|
keepalive: true
|
|
112
391
|
}).catch(() => {
|
|
113
392
|
});
|
|
@@ -124,14 +403,12 @@ var EventBuffer = class {
|
|
|
124
403
|
});
|
|
125
404
|
}
|
|
126
405
|
};
|
|
127
|
-
|
|
128
|
-
function installAutoGuardrails(buffer, userId, anonId, groups) {
|
|
406
|
+
function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignoreUrlPrefixes, always = false) {
|
|
129
407
|
if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") return;
|
|
408
|
+
const shouldEmit = () => always || buffer.hasExposures();
|
|
130
409
|
let lcp = null;
|
|
131
410
|
let inp = null;
|
|
132
411
|
let clsBad = false;
|
|
133
|
-
let jsErrorCount = 0;
|
|
134
|
-
let netErrorCount = 0;
|
|
135
412
|
let navTimingFlushed = false;
|
|
136
413
|
if (groups.vitals) {
|
|
137
414
|
try {
|
|
@@ -170,68 +447,71 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
|
|
|
170
447
|
if (groups.errors) {
|
|
171
448
|
const origOnError = window.onerror;
|
|
172
449
|
window.onerror = (msg, source, lineno, _colno, err) => {
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
+
);
|
|
182
461
|
}
|
|
183
462
|
if (typeof origOnError === "function") return origOnError(msg, source, lineno, _colno, err);
|
|
184
463
|
return false;
|
|
185
464
|
};
|
|
186
465
|
window.addEventListener("unhandledrejection", (e) => {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
});
|
|
196
|
-
}
|
|
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
|
+
);
|
|
197
474
|
});
|
|
198
475
|
const origFetch = window.fetch;
|
|
199
476
|
window.fetch = async function(...args) {
|
|
200
477
|
const startedAt = typeof performance !== "undefined" ? performance.now() : 0;
|
|
201
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);
|
|
202
481
|
let res;
|
|
203
482
|
try {
|
|
204
483
|
res = await origFetch.apply(this, args);
|
|
205
484
|
} catch (err) {
|
|
206
|
-
if (
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
});
|
|
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
|
+
);
|
|
214
492
|
}
|
|
215
493
|
throw err;
|
|
216
494
|
}
|
|
217
|
-
if (res.status >= 500
|
|
218
|
-
netErrorCount += 1;
|
|
495
|
+
if (!ignored && res.status >= 500) {
|
|
219
496
|
const elapsed = typeof performance !== "undefined" ? performance.now() - startedAt : 0;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
status: res.status,
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
});
|
|
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
|
+
);
|
|
227
503
|
}
|
|
228
504
|
return res;
|
|
229
505
|
};
|
|
230
506
|
}
|
|
231
507
|
const flushNavTiming = () => {
|
|
232
508
|
if (navTimingFlushed) return;
|
|
509
|
+
if (!groups.vitals) {
|
|
510
|
+
navTimingFlushed = true;
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (!shouldEmit()) return;
|
|
233
514
|
navTimingFlushed = true;
|
|
234
|
-
if (!groups.vitals) return;
|
|
235
515
|
try {
|
|
236
516
|
const navList = performance.getEntriesByType("navigation");
|
|
237
517
|
const nav = navList[0];
|
|
@@ -266,7 +546,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
|
|
|
266
546
|
};
|
|
267
547
|
if (groups.engagement) {
|
|
268
548
|
try {
|
|
269
|
-
buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
|
|
549
|
+
if (shouldEmit()) buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
|
|
270
550
|
} catch {
|
|
271
551
|
}
|
|
272
552
|
let lastEmit = Date.now();
|
|
@@ -274,6 +554,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
|
|
|
274
554
|
document.addEventListener("visibilitychange", () => {
|
|
275
555
|
if (document.visibilityState !== "visible") return;
|
|
276
556
|
if (Date.now() - lastEmit < SESSION_GAP_MS) return;
|
|
557
|
+
if (!shouldEmit()) return;
|
|
277
558
|
try {
|
|
278
559
|
buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
|
|
279
560
|
lastEmit = Date.now();
|
|
@@ -296,7 +577,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
|
|
|
296
577
|
}
|
|
297
578
|
const flushOnHide = () => {
|
|
298
579
|
flushNavTiming();
|
|
299
|
-
if (groups.vitals) {
|
|
580
|
+
if (groups.vitals && shouldEmit()) {
|
|
300
581
|
if (lcp !== null) buffer.pushMetric("__auto_lcp", userId, anonId, { value: lcp });
|
|
301
582
|
if (inp !== null) buffer.pushMetric("__auto_inp", userId, anonId, { value: inp });
|
|
302
583
|
if (clsBad) buffer.pushMetric("__auto_cls_binary", userId, anonId, { value: 1 });
|
|
@@ -379,11 +660,14 @@ var FlagsClientBrowser = class {
|
|
|
379
660
|
baseUrl;
|
|
380
661
|
autoGuardrails;
|
|
381
662
|
autoGuardrailGroups;
|
|
663
|
+
autoCollectAlways;
|
|
382
664
|
env;
|
|
383
665
|
evalResult = null;
|
|
384
666
|
anonId;
|
|
385
667
|
userId = "";
|
|
386
668
|
buffer;
|
|
669
|
+
telemetry;
|
|
670
|
+
seeLimiter = new SeeLimiter();
|
|
387
671
|
guardrailsInstalled = false;
|
|
388
672
|
listeners = /* @__PURE__ */ new Set();
|
|
389
673
|
overrideListenerInstalled = false;
|
|
@@ -399,6 +683,7 @@ var FlagsClientBrowser = class {
|
|
|
399
683
|
this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
|
|
400
684
|
this.env = opts.env ?? "prod";
|
|
401
685
|
this.autoGuardrails = opts.autoGuardrails !== false;
|
|
686
|
+
this.autoCollectAlways = opts.autoCollectAlways === true;
|
|
402
687
|
const g = opts.autoGuardrailGroups ?? {};
|
|
403
688
|
this.autoGuardrailGroups = {
|
|
404
689
|
vitals: g.vitals ?? this.autoGuardrails,
|
|
@@ -407,6 +692,13 @@ var FlagsClientBrowser = class {
|
|
|
407
692
|
};
|
|
408
693
|
this.anonId = getOrCreateAnonId();
|
|
409
694
|
this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
|
|
695
|
+
this.telemetry = new Telemetry({
|
|
696
|
+
endpoint: opts.telemetryUrl ?? DEFAULT_TELEMETRY_URL,
|
|
697
|
+
sdkKey: this.sdkKey,
|
|
698
|
+
side: "client",
|
|
699
|
+
env: this.env,
|
|
700
|
+
disabled: opts.disableTelemetry
|
|
701
|
+
});
|
|
410
702
|
void this.buffer.flushPendingAlias();
|
|
411
703
|
}
|
|
412
704
|
async identify(user) {
|
|
@@ -436,10 +728,38 @@ var FlagsClientBrowser = class {
|
|
|
436
728
|
const anyGroupOn = this.autoGuardrailGroups.vitals || this.autoGuardrailGroups.errors || this.autoGuardrailGroups.engagement;
|
|
437
729
|
if (anyGroupOn && !this.guardrailsInstalled) {
|
|
438
730
|
this.guardrailsInstalled = true;
|
|
439
|
-
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
|
+
);
|
|
440
740
|
}
|
|
441
741
|
this.notify();
|
|
442
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
|
+
}
|
|
443
763
|
get ready() {
|
|
444
764
|
return this.evalResult !== null;
|
|
445
765
|
}
|
|
@@ -456,12 +776,14 @@ var FlagsClientBrowser = class {
|
|
|
456
776
|
this.evalResult = data;
|
|
457
777
|
}
|
|
458
778
|
getFlag(name) {
|
|
779
|
+
this.telemetry.emit("gate", name);
|
|
459
780
|
if (this.evalResult === null) return false;
|
|
460
781
|
const ov = readGateOverride(name);
|
|
461
782
|
if (ov !== null) return ov;
|
|
462
783
|
return this.evalResult.flags[name] ?? false;
|
|
463
784
|
}
|
|
464
785
|
getConfig(name, decode) {
|
|
786
|
+
this.telemetry.emit("config", name);
|
|
465
787
|
if (this.evalResult === null) return void 0;
|
|
466
788
|
const ov = readConfigOverride(name);
|
|
467
789
|
const raw = ov !== void 0 ? ov : this.evalResult.configs?.[name];
|
|
@@ -475,6 +797,7 @@ var FlagsClientBrowser = class {
|
|
|
475
797
|
}
|
|
476
798
|
}
|
|
477
799
|
getExperiment(name, defaultParams, decode, variants) {
|
|
800
|
+
this.telemetry.emit("experiment", name);
|
|
478
801
|
const notIn = {
|
|
479
802
|
inExperiment: false,
|
|
480
803
|
group: "control",
|
|
@@ -539,6 +862,7 @@ var FlagsClientBrowser = class {
|
|
|
539
862
|
* the per-switch state. Returns false for unknown killswitches / switches.
|
|
540
863
|
*/
|
|
541
864
|
getKillswitch(name, switchKey) {
|
|
865
|
+
this.telemetry.emit("ks", name);
|
|
542
866
|
if (this.evalResult === null) return false;
|
|
543
867
|
const ks = this.evalResult.killswitches?.[name];
|
|
544
868
|
if (ks === void 0) return false;
|
|
@@ -667,13 +991,16 @@ var _client = null;
|
|
|
667
991
|
function shipeasy(opts) {
|
|
668
992
|
const ac = opts.autoCollect;
|
|
669
993
|
const blanket = ac === false ? false : true;
|
|
670
|
-
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;
|
|
671
996
|
const baseUrl = opts.baseUrl ?? "https://cdn.shipeasy.ai";
|
|
672
997
|
const client = configureShipeasy({
|
|
673
998
|
sdkKey: opts.clientKey,
|
|
674
999
|
baseUrl,
|
|
675
1000
|
autoGuardrails: blanket,
|
|
676
|
-
autoGuardrailGroups: groups
|
|
1001
|
+
autoGuardrailGroups: groups,
|
|
1002
|
+
autoCollectAlways: acObj?.always === true,
|
|
1003
|
+
disableTelemetry: opts.disableTelemetry
|
|
677
1004
|
});
|
|
678
1005
|
injectI18nLoader(opts.clientKey, baseUrl, opts.i18nProfile);
|
|
679
1006
|
flags.notifyMounted();
|
|
@@ -834,6 +1161,20 @@ var flags = {
|
|
|
834
1161
|
return _client?.ready ?? false;
|
|
835
1162
|
}
|
|
836
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
|
+
);
|
|
837
1178
|
var LABEL_MARKER_START = "\uFFF9";
|
|
838
1179
|
var LABEL_MARKER_SEP = "\uFFFA";
|
|
839
1180
|
var LABEL_MARKER_END = "\uFFFB";
|
|
@@ -1085,6 +1426,7 @@ export {
|
|
|
1085
1426
|
readConfigOverride,
|
|
1086
1427
|
readExpOverride,
|
|
1087
1428
|
readGateOverride,
|
|
1429
|
+
see,
|
|
1088
1430
|
shipeasy,
|
|
1089
1431
|
version
|
|
1090
1432
|
};
|