@shipeasy/sdk 3.1.0 → 4.2.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 +171 -7
- package/dist/client/index.d.ts +171 -7
- package/dist/client/index.js +454 -50
- package/dist/client/index.mjs +453 -50
- package/dist/server/index.d.mts +134 -2
- package/dist/server/index.d.ts +134 -2
- package/dist/server/index.js +278 -1
- package/dist/server/index.mjs +277 -1
- package/package.json +1 -1
package/dist/client/index.mjs
CHANGED
|
@@ -57,8 +57,251 @@ 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
|
+
var REPORTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-reported");
|
|
110
|
+
var SEE_MAX_CAUSE_DEPTH = 8;
|
|
111
|
+
function readReportStamp(err) {
|
|
112
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
113
|
+
const v = err[REPORTED_SYM];
|
|
114
|
+
return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
|
|
115
|
+
}
|
|
116
|
+
function findCausedBy(problem) {
|
|
117
|
+
let cur = problem;
|
|
118
|
+
const seen = /* @__PURE__ */ new Set();
|
|
119
|
+
for (let depth = 0; depth < SEE_MAX_CAUSE_DEPTH; depth++) {
|
|
120
|
+
if (typeof cur !== "object" || cur === null || seen.has(cur)) break;
|
|
121
|
+
seen.add(cur);
|
|
122
|
+
const stamp = readReportStamp(cur);
|
|
123
|
+
if (stamp) return stamp;
|
|
124
|
+
cur = cur.cause;
|
|
125
|
+
}
|
|
126
|
+
return void 0;
|
|
127
|
+
}
|
|
128
|
+
function markReported(problem, ev) {
|
|
129
|
+
if (!(problem instanceof Error)) return;
|
|
130
|
+
const stamp = {
|
|
131
|
+
error_type: ev.error_type,
|
|
132
|
+
message: ev.message,
|
|
133
|
+
subject: ev.subject,
|
|
134
|
+
outcome: ev.outcome
|
|
135
|
+
};
|
|
136
|
+
if (ev.stack !== void 0) stamp.stack = ev.stack;
|
|
137
|
+
try {
|
|
138
|
+
Object.defineProperty(problem, REPORTED_SYM, {
|
|
139
|
+
value: Object.freeze(stamp),
|
|
140
|
+
enumerable: false,
|
|
141
|
+
configurable: true,
|
|
142
|
+
writable: true
|
|
143
|
+
});
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function truncate(s, max) {
|
|
148
|
+
return s.length > max ? s.slice(0, max) : s;
|
|
149
|
+
}
|
|
150
|
+
function sanitizeExtras(extras) {
|
|
151
|
+
if (!extras || typeof extras !== "object") return void 0;
|
|
152
|
+
const out = {};
|
|
153
|
+
let n = 0;
|
|
154
|
+
for (const [k, v] of Object.entries(extras)) {
|
|
155
|
+
if (v === null || v === void 0) continue;
|
|
156
|
+
if (n >= SEE_MAX_EXTRA_KEYS) break;
|
|
157
|
+
if (typeof v === "string") out[k] = truncate(v, SEE_MAX_EXTRA_VALUE);
|
|
158
|
+
else if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
|
|
159
|
+
else if (typeof v === "boolean") out[k] = v;
|
|
160
|
+
else continue;
|
|
161
|
+
n += 1;
|
|
162
|
+
}
|
|
163
|
+
return n > 0 ? out : void 0;
|
|
164
|
+
}
|
|
165
|
+
function captureCallsiteStack() {
|
|
166
|
+
const raw = new Error().stack;
|
|
167
|
+
if (!raw) return void 0;
|
|
168
|
+
const lines = raw.split("\n");
|
|
169
|
+
const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
|
|
170
|
+
return kept.length ? kept.join("\n") : void 0;
|
|
171
|
+
}
|
|
172
|
+
function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
|
|
173
|
+
let errorType;
|
|
174
|
+
let message;
|
|
175
|
+
let stack;
|
|
176
|
+
let kind;
|
|
177
|
+
if (isViolation(problem)) {
|
|
178
|
+
errorType = problem.violationName;
|
|
179
|
+
message = problem.violationMessage ?? problem.violationName;
|
|
180
|
+
stack = captureCallsiteStack();
|
|
181
|
+
kind = kindOverride ?? "violation";
|
|
182
|
+
} else if (problem instanceof Error) {
|
|
183
|
+
errorType = problem.name || "Error";
|
|
184
|
+
message = problem.message || String(problem);
|
|
185
|
+
stack = problem.stack ?? void 0;
|
|
186
|
+
kind = kindOverride ?? "caught";
|
|
187
|
+
} else {
|
|
188
|
+
errorType = "Error";
|
|
189
|
+
message = typeof problem === "string" ? problem : safeString(problem);
|
|
190
|
+
stack = captureCallsiteStack();
|
|
191
|
+
kind = kindOverride ?? "caught";
|
|
192
|
+
}
|
|
193
|
+
const ev = {
|
|
194
|
+
type: "error",
|
|
195
|
+
kind,
|
|
196
|
+
error_type: truncate(errorType, SEE_MAX_SUBJECT),
|
|
197
|
+
message: truncate(message, SEE_MAX_MESSAGE),
|
|
198
|
+
subject: consequence.subject,
|
|
199
|
+
outcome: consequence.outcome,
|
|
200
|
+
side: ctx.side,
|
|
201
|
+
sdk_version: ctx.sdkVersion,
|
|
202
|
+
ts: Date.now()
|
|
203
|
+
};
|
|
204
|
+
if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
|
|
205
|
+
const causedBy = findCausedBy(problem);
|
|
206
|
+
if (causedBy) ev.caused_by = causedBy;
|
|
207
|
+
const cleanExtras = sanitizeExtras(extras);
|
|
208
|
+
if (cleanExtras) ev.extras = cleanExtras;
|
|
209
|
+
if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
|
|
210
|
+
if (ctx.userId) ev.user_id = ctx.userId;
|
|
211
|
+
if (ctx.anonId) ev.anonymous_id = ctx.anonId;
|
|
212
|
+
if (ctx.env) ev.env = ctx.env;
|
|
213
|
+
markReported(problem, ev);
|
|
214
|
+
return ev;
|
|
215
|
+
}
|
|
216
|
+
function safeString(v) {
|
|
217
|
+
try {
|
|
218
|
+
return typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
219
|
+
} catch {
|
|
220
|
+
return String(v);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
var scheduleMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : (cb) => {
|
|
224
|
+
void Promise.resolve().then(cb);
|
|
225
|
+
};
|
|
226
|
+
function startSeeChain(getProblem, dispatch) {
|
|
227
|
+
let subject;
|
|
228
|
+
let outcome;
|
|
229
|
+
let collected;
|
|
230
|
+
let flushed = false;
|
|
231
|
+
scheduleMicrotask(() => {
|
|
232
|
+
if (flushed) return;
|
|
233
|
+
flushed = true;
|
|
234
|
+
dispatch(
|
|
235
|
+
getProblem(),
|
|
236
|
+
// Bare noun phrase — titles render as "… causes the {subject} …", so a
|
|
237
|
+
// leading article would double up ("causes the the app").
|
|
238
|
+
causesThe(subject ?? "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
|
+
|
|
60
303
|
// src/client/index.ts
|
|
61
|
-
var version = "
|
|
304
|
+
var version = "4.0.0";
|
|
62
305
|
var FLUSH_INTERVAL_MS = 5e3;
|
|
63
306
|
var MAX_BUFFER = 100;
|
|
64
307
|
var ANON_ID_KEY = "__se_anon_id";
|
|
@@ -92,6 +335,13 @@ var EventBuffer = class {
|
|
|
92
335
|
this.timer = null;
|
|
93
336
|
}
|
|
94
337
|
}
|
|
338
|
+
/** True once this visitor has been exposed to ≥1 experiment (this tab or a
|
|
339
|
+
* prior page in the session — the dedup set persists in sessionStorage).
|
|
340
|
+
* Gates auto-metric emission: vitals from non-participants are never read
|
|
341
|
+
* by the analysis pipeline and would be pure AE write cost (see cost.md). */
|
|
342
|
+
hasExposures() {
|
|
343
|
+
return this.exposureSeen.size > 0;
|
|
344
|
+
}
|
|
95
345
|
pushExposure(experiment, group, userId, anonId) {
|
|
96
346
|
const key = `${userId || anonId}:${experiment}`;
|
|
97
347
|
if (this.exposureSeen.has(key)) return;
|
|
@@ -157,16 +407,29 @@ var EventBuffer = class {
|
|
|
157
407
|
flush(useBeacon = false) {
|
|
158
408
|
if (!this.queue.length) return;
|
|
159
409
|
const batch = this.queue.splice(0);
|
|
160
|
-
|
|
410
|
+
this.send(batch, useBeacon);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Bypass the 5s queue and ship events immediately — used by see() error
|
|
414
|
+
* reporting so occurrences land near-real-time and survive page unload.
|
|
415
|
+
* Beacon-first (fire-and-forget, unload-safe), keepalive fetch fallback.
|
|
416
|
+
*/
|
|
417
|
+
sendNow(events) {
|
|
418
|
+
this.send(events, true);
|
|
419
|
+
}
|
|
420
|
+
send(batch, useBeacon) {
|
|
161
421
|
if (useBeacon && typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
162
422
|
const beaconBody = JSON.stringify({ k: this.sdkKey, events: batch });
|
|
163
|
-
|
|
164
|
-
|
|
423
|
+
try {
|
|
424
|
+
if (navigator.sendBeacon(this.collectUrl, new Blob([beaconBody], { type: "text/plain" })))
|
|
425
|
+
return;
|
|
426
|
+
} catch {
|
|
427
|
+
}
|
|
165
428
|
}
|
|
166
429
|
fetch(this.collectUrl, {
|
|
167
430
|
method: "POST",
|
|
168
431
|
headers: { "X-SDK-Key": this.sdkKey, "Content-Type": "application/json" },
|
|
169
|
-
body,
|
|
432
|
+
body: JSON.stringify({ events: batch }),
|
|
170
433
|
keepalive: true
|
|
171
434
|
}).catch(() => {
|
|
172
435
|
});
|
|
@@ -183,14 +446,24 @@ var EventBuffer = class {
|
|
|
183
446
|
});
|
|
184
447
|
}
|
|
185
448
|
};
|
|
186
|
-
|
|
187
|
-
|
|
449
|
+
function endpointTemplate(rawUrl) {
|
|
450
|
+
const isIdSegment = (seg) => /^\d+$/.test(seg) || /^0x[0-9a-f]+$/i.test(seg) || /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(seg) || /^[0-9a-f]{8,}$/i.test(seg) || seg.length >= 12 && /\d/.test(seg) && /[a-z]/i.test(seg);
|
|
451
|
+
let u;
|
|
452
|
+
try {
|
|
453
|
+
u = new URL(rawUrl, typeof location !== "undefined" ? location.href : void 0);
|
|
454
|
+
} catch {
|
|
455
|
+
return (rawUrl.split(/[?#]/)[0] ?? "").slice(0, 120);
|
|
456
|
+
}
|
|
457
|
+
const path = u.pathname.split("/").map((seg) => seg && isIdSegment(seg) ? ":id" : seg).join("/");
|
|
458
|
+
const sameOrigin = typeof location !== "undefined" && u.origin === location.origin;
|
|
459
|
+
return ((sameOrigin ? "" : u.host) + path).slice(0, 120);
|
|
460
|
+
}
|
|
461
|
+
function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignoreUrlPrefixes, always = false) {
|
|
188
462
|
if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") return;
|
|
463
|
+
const shouldEmit = () => always || buffer.hasExposures();
|
|
189
464
|
let lcp = null;
|
|
190
465
|
let inp = null;
|
|
191
466
|
let clsBad = false;
|
|
192
|
-
let jsErrorCount = 0;
|
|
193
|
-
let netErrorCount = 0;
|
|
194
467
|
let navTimingFlushed = false;
|
|
195
468
|
if (groups.vitals) {
|
|
196
469
|
try {
|
|
@@ -229,68 +502,71 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
|
|
|
229
502
|
if (groups.errors) {
|
|
230
503
|
const origOnError = window.onerror;
|
|
231
504
|
window.onerror = (msg, source, lineno, _colno, err) => {
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
505
|
+
if (!isExpected(err)) {
|
|
506
|
+
const problem = err ?? (typeof msg === "string" && msg ? msg : "Unknown error");
|
|
507
|
+
reportSee(
|
|
508
|
+
problem,
|
|
509
|
+
causesThe("page").to("hit an unhandled error"),
|
|
510
|
+
{
|
|
511
|
+
source: typeof source === "string" ? source : void 0,
|
|
512
|
+
line: lineno ?? void 0
|
|
513
|
+
},
|
|
514
|
+
"uncaught"
|
|
515
|
+
);
|
|
241
516
|
}
|
|
242
517
|
if (typeof origOnError === "function") return origOnError(msg, source, lineno, _colno, err);
|
|
243
518
|
return false;
|
|
244
519
|
};
|
|
245
520
|
window.addEventListener("unhandledrejection", (e) => {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
});
|
|
255
|
-
}
|
|
521
|
+
const reason = e.reason;
|
|
522
|
+
if (isExpected(reason)) return;
|
|
523
|
+
reportSee(
|
|
524
|
+
reason ?? "Unhandled promise rejection",
|
|
525
|
+
causesThe("page").to("hit an unhandled promise rejection"),
|
|
526
|
+
void 0,
|
|
527
|
+
"unhandled_rejection"
|
|
528
|
+
);
|
|
256
529
|
});
|
|
257
530
|
const origFetch = window.fetch;
|
|
258
531
|
window.fetch = async function(...args) {
|
|
259
532
|
const startedAt = typeof performance !== "undefined" ? performance.now() : 0;
|
|
260
533
|
const url = typeof args[0] === "string" ? args[0] : args[0].toString();
|
|
534
|
+
const ignored = ignoreUrlPrefixes.some((p) => p && url.startsWith(p));
|
|
535
|
+
const bareUrl = url.split("?")[0].slice(0, 200);
|
|
261
536
|
let res;
|
|
262
537
|
try {
|
|
263
538
|
res = await origFetch.apply(this, args);
|
|
264
539
|
} catch (err) {
|
|
265
|
-
if (
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
});
|
|
540
|
+
if (!ignored && !isExpected(err)) {
|
|
541
|
+
reportSee(
|
|
542
|
+
violation("NetworkError").message(`request to ${bareUrl} failed`),
|
|
543
|
+
causesThe(`request to ${endpointTemplate(url)}`).to("get no response"),
|
|
544
|
+
{ status: 0, url: url.slice(0, 200) },
|
|
545
|
+
"network"
|
|
546
|
+
);
|
|
273
547
|
}
|
|
274
548
|
throw err;
|
|
275
549
|
}
|
|
276
|
-
if (res.status >= 500
|
|
277
|
-
netErrorCount += 1;
|
|
550
|
+
if (!ignored && res.status >= 500) {
|
|
278
551
|
const elapsed = typeof performance !== "undefined" ? performance.now() - startedAt : 0;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
status: res.status,
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
});
|
|
552
|
+
reportSee(
|
|
553
|
+
violation("Http5xx").message(`request to ${bareUrl} returned ${res.status}`),
|
|
554
|
+
causesThe(`request to ${endpointTemplate(url)}`).to("fail with a server error"),
|
|
555
|
+
{ status: res.status, url: url.slice(0, 200), duration_ms: Math.round(elapsed) },
|
|
556
|
+
"network"
|
|
557
|
+
);
|
|
286
558
|
}
|
|
287
559
|
return res;
|
|
288
560
|
};
|
|
289
561
|
}
|
|
290
562
|
const flushNavTiming = () => {
|
|
291
563
|
if (navTimingFlushed) return;
|
|
564
|
+
if (!groups.vitals) {
|
|
565
|
+
navTimingFlushed = true;
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (!shouldEmit()) return;
|
|
292
569
|
navTimingFlushed = true;
|
|
293
|
-
if (!groups.vitals) return;
|
|
294
570
|
try {
|
|
295
571
|
const navList = performance.getEntriesByType("navigation");
|
|
296
572
|
const nav = navList[0];
|
|
@@ -325,7 +601,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
|
|
|
325
601
|
};
|
|
326
602
|
if (groups.engagement) {
|
|
327
603
|
try {
|
|
328
|
-
buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
|
|
604
|
+
if (shouldEmit()) buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
|
|
329
605
|
} catch {
|
|
330
606
|
}
|
|
331
607
|
let lastEmit = Date.now();
|
|
@@ -333,6 +609,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
|
|
|
333
609
|
document.addEventListener("visibilitychange", () => {
|
|
334
610
|
if (document.visibilityState !== "visible") return;
|
|
335
611
|
if (Date.now() - lastEmit < SESSION_GAP_MS) return;
|
|
612
|
+
if (!shouldEmit()) return;
|
|
336
613
|
try {
|
|
337
614
|
buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
|
|
338
615
|
lastEmit = Date.now();
|
|
@@ -355,7 +632,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups) {
|
|
|
355
632
|
}
|
|
356
633
|
const flushOnHide = () => {
|
|
357
634
|
flushNavTiming();
|
|
358
|
-
if (groups.vitals) {
|
|
635
|
+
if (groups.vitals && shouldEmit()) {
|
|
359
636
|
if (lcp !== null) buffer.pushMetric("__auto_lcp", userId, anonId, { value: lcp });
|
|
360
637
|
if (inp !== null) buffer.pushMetric("__auto_inp", userId, anonId, { value: inp });
|
|
361
638
|
if (clsBad) buffer.pushMetric("__auto_cls_binary", userId, anonId, { value: 1 });
|
|
@@ -419,6 +696,83 @@ function collectBrowserAttrs() {
|
|
|
419
696
|
}
|
|
420
697
|
return attrs;
|
|
421
698
|
}
|
|
699
|
+
function collectSeeEnv() {
|
|
700
|
+
const out = {};
|
|
701
|
+
if (typeof navigator === "undefined") return out;
|
|
702
|
+
const nav = navigator;
|
|
703
|
+
const ua = typeof nav.userAgent === "string" ? nav.userAgent : "";
|
|
704
|
+
const browser = parseUaBrowser(ua);
|
|
705
|
+
if (browser) out["env.browser"] = browser;
|
|
706
|
+
const os = parseUaOs(ua) ?? nav.userAgentData?.platform;
|
|
707
|
+
if (os) out["env.os"] = os;
|
|
708
|
+
out["env.device"] = typeof nav.userAgentData?.mobile === "boolean" ? nav.userAgentData.mobile ? "mobile" : "desktop" : /iPad|Tablet/.test(ua) ? "tablet" : /Mobi|iPhone|Android.*Mobile/.test(ua) ? "mobile" : "desktop";
|
|
709
|
+
try {
|
|
710
|
+
if (nav.language) out["env.lang"] = nav.language;
|
|
711
|
+
} catch {
|
|
712
|
+
}
|
|
713
|
+
try {
|
|
714
|
+
if (typeof nav.onLine === "boolean") out["env.online"] = nav.onLine;
|
|
715
|
+
} catch {
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
if (typeof nav.hardwareConcurrency === "number") out["env.cores"] = nav.hardwareConcurrency;
|
|
719
|
+
} catch {
|
|
720
|
+
}
|
|
721
|
+
try {
|
|
722
|
+
if (typeof nav.deviceMemory === "number") out["env.memory_gb"] = nav.deviceMemory;
|
|
723
|
+
} catch {
|
|
724
|
+
}
|
|
725
|
+
try {
|
|
726
|
+
const et = nav.connection?.effectiveType;
|
|
727
|
+
if (et) out["env.connection"] = et;
|
|
728
|
+
} catch {
|
|
729
|
+
}
|
|
730
|
+
try {
|
|
731
|
+
if (typeof window !== "undefined" && window.innerWidth && window.innerHeight) {
|
|
732
|
+
out["env.viewport"] = `${window.innerWidth}\xD7${window.innerHeight}`;
|
|
733
|
+
}
|
|
734
|
+
if (typeof window !== "undefined" && typeof window.devicePixelRatio === "number") {
|
|
735
|
+
out["env.dpr"] = window.devicePixelRatio;
|
|
736
|
+
}
|
|
737
|
+
if (typeof screen !== "undefined" && screen.width && screen.height) {
|
|
738
|
+
out["env.screen"] = `${screen.width}\xD7${screen.height}`;
|
|
739
|
+
}
|
|
740
|
+
} catch {
|
|
741
|
+
}
|
|
742
|
+
try {
|
|
743
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
744
|
+
if (tz) out["env.tz"] = tz;
|
|
745
|
+
} catch {
|
|
746
|
+
}
|
|
747
|
+
return out;
|
|
748
|
+
}
|
|
749
|
+
function parseUaBrowser(ua) {
|
|
750
|
+
const tests = [
|
|
751
|
+
[/Edg(?:A|iOS)?\/(\d+)/, "Edge"],
|
|
752
|
+
[/(?:OPR|Opera)\/(\d+)/, "Opera"],
|
|
753
|
+
[/(?:Firefox|FxiOS)\/(\d+)/, "Firefox"],
|
|
754
|
+
[/(?:Chrome|CriOS)\/(\d+)/, "Chrome"],
|
|
755
|
+
[/Version\/(\d+)[.\d]* (?:Mobile.*)?Safari/, "Safari"]
|
|
756
|
+
];
|
|
757
|
+
for (const [re, name] of tests) {
|
|
758
|
+
const m = re.exec(ua);
|
|
759
|
+
if (m) return `${name} ${m[1]}`;
|
|
760
|
+
}
|
|
761
|
+
return void 0;
|
|
762
|
+
}
|
|
763
|
+
function parseUaOs(ua) {
|
|
764
|
+
if (/Windows NT 10/.test(ua)) return "Windows 10/11";
|
|
765
|
+
if (/Windows NT/.test(ua)) return "Windows";
|
|
766
|
+
let m = /Mac OS X (\d+)[._](\d+)/.exec(ua);
|
|
767
|
+
if (m) return `macOS ${m[1]}.${m[2]}`;
|
|
768
|
+
if (/Macintosh/.test(ua)) return "macOS";
|
|
769
|
+
m = /Android (\d+)/.exec(ua);
|
|
770
|
+
if (m) return `Android ${m[1]}`;
|
|
771
|
+
m = /(?:iPhone|iPad)[^)]* OS (\d+)/.exec(ua);
|
|
772
|
+
if (m) return `iOS ${m[1]}`;
|
|
773
|
+
if (/Linux/.test(ua)) return "Linux";
|
|
774
|
+
return void 0;
|
|
775
|
+
}
|
|
422
776
|
function readExperimentOverridesFromUrl() {
|
|
423
777
|
if (typeof window === "undefined") return {};
|
|
424
778
|
const out = {};
|
|
@@ -438,12 +792,14 @@ var FlagsClientBrowser = class {
|
|
|
438
792
|
baseUrl;
|
|
439
793
|
autoGuardrails;
|
|
440
794
|
autoGuardrailGroups;
|
|
795
|
+
autoCollectAlways;
|
|
441
796
|
env;
|
|
442
797
|
evalResult = null;
|
|
443
798
|
anonId;
|
|
444
799
|
userId = "";
|
|
445
800
|
buffer;
|
|
446
801
|
telemetry;
|
|
802
|
+
seeLimiter = new SeeLimiter();
|
|
447
803
|
guardrailsInstalled = false;
|
|
448
804
|
listeners = /* @__PURE__ */ new Set();
|
|
449
805
|
overrideListenerInstalled = false;
|
|
@@ -459,6 +815,7 @@ var FlagsClientBrowser = class {
|
|
|
459
815
|
this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
|
|
460
816
|
this.env = opts.env ?? "prod";
|
|
461
817
|
this.autoGuardrails = opts.autoGuardrails !== false;
|
|
818
|
+
this.autoCollectAlways = opts.autoCollectAlways === true;
|
|
462
819
|
const g = opts.autoGuardrailGroups ?? {};
|
|
463
820
|
this.autoGuardrailGroups = {
|
|
464
821
|
vitals: g.vitals ?? this.autoGuardrails,
|
|
@@ -503,10 +860,39 @@ var FlagsClientBrowser = class {
|
|
|
503
860
|
const anyGroupOn = this.autoGuardrailGroups.vitals || this.autoGuardrailGroups.errors || this.autoGuardrailGroups.engagement;
|
|
504
861
|
if (anyGroupOn && !this.guardrailsInstalled) {
|
|
505
862
|
this.guardrailsInstalled = true;
|
|
506
|
-
installAutoGuardrails(
|
|
863
|
+
installAutoGuardrails(
|
|
864
|
+
this.buffer,
|
|
865
|
+
this.userId,
|
|
866
|
+
this.anonId,
|
|
867
|
+
this.autoGuardrailGroups,
|
|
868
|
+
(problem, consequence, extras, kind) => this.reportError(problem, consequence, extras, kind),
|
|
869
|
+
[`${this.baseUrl}/`, DEFAULT_TELEMETRY_URL],
|
|
870
|
+
this.autoCollectAlways
|
|
871
|
+
);
|
|
507
872
|
}
|
|
508
873
|
this.notify();
|
|
509
874
|
}
|
|
875
|
+
/**
|
|
876
|
+
* Report a structured error into the errors primitive. Flushes immediately
|
|
877
|
+
* (beacon-first) — error occurrences are near-real-time, never queued behind
|
|
878
|
+
* the 5s metric batch. Spam-guarded by a 30s dedup window + per-session cap.
|
|
879
|
+
*/
|
|
880
|
+
reportError(problem, consequence, extras, kind) {
|
|
881
|
+
try {
|
|
882
|
+
const enriched = { ...collectSeeEnv(), ...extras };
|
|
883
|
+
const ev = buildSeeEvent(problem, consequence, enriched, {
|
|
884
|
+
side: "client",
|
|
885
|
+
sdkVersion: version,
|
|
886
|
+
env: this.env,
|
|
887
|
+
url: typeof window !== "undefined" && window.location ? window.location.href : void 0,
|
|
888
|
+
userId: this.userId || void 0,
|
|
889
|
+
anonId: this.anonId
|
|
890
|
+
}, kind);
|
|
891
|
+
if (!this.seeLimiter.shouldSend(ev)) return;
|
|
892
|
+
this.buffer.sendNow([ev]);
|
|
893
|
+
} catch {
|
|
894
|
+
}
|
|
895
|
+
}
|
|
510
896
|
get ready() {
|
|
511
897
|
return this.evalResult !== null;
|
|
512
898
|
}
|
|
@@ -738,13 +1124,15 @@ var _client = null;
|
|
|
738
1124
|
function shipeasy(opts) {
|
|
739
1125
|
const ac = opts.autoCollect;
|
|
740
1126
|
const blanket = ac === false ? false : true;
|
|
741
|
-
const
|
|
1127
|
+
const acObj = ac && typeof ac === "object" ? ac : void 0;
|
|
1128
|
+
const groups = acObj ? { vitals: acObj.vitals, errors: acObj.errors, engagement: acObj.engagement } : void 0;
|
|
742
1129
|
const baseUrl = opts.baseUrl ?? "https://cdn.shipeasy.ai";
|
|
743
1130
|
const client = configureShipeasy({
|
|
744
1131
|
sdkKey: opts.clientKey,
|
|
745
1132
|
baseUrl,
|
|
746
1133
|
autoGuardrails: blanket,
|
|
747
1134
|
autoGuardrailGroups: groups,
|
|
1135
|
+
autoCollectAlways: acObj?.always === true,
|
|
748
1136
|
disableTelemetry: opts.disableTelemetry
|
|
749
1137
|
});
|
|
750
1138
|
injectI18nLoader(opts.clientKey, baseUrl, opts.i18nProfile);
|
|
@@ -906,6 +1294,20 @@ var flags = {
|
|
|
906
1294
|
return _client?.ready ?? false;
|
|
907
1295
|
}
|
|
908
1296
|
};
|
|
1297
|
+
function dispatchSee(problem, consequence, extras, kind) {
|
|
1298
|
+
if (!_client) {
|
|
1299
|
+
console.warn("[shipeasy] see() called before shipeasy({ clientKey }) \u2014 error dropped");
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
_client.reportError(problem, consequence, extras, kind);
|
|
1303
|
+
}
|
|
1304
|
+
var see = Object.assign(
|
|
1305
|
+
(problem) => startSeeChain(() => problem, dispatchSee),
|
|
1306
|
+
{
|
|
1307
|
+
Violation: (name) => startSeeViolationChain(name, dispatchSee),
|
|
1308
|
+
ControlFlowException: markExpected
|
|
1309
|
+
}
|
|
1310
|
+
);
|
|
909
1311
|
var LABEL_MARKER_START = "\uFFF9";
|
|
910
1312
|
var LABEL_MARKER_SEP = "\uFFFA";
|
|
911
1313
|
var LABEL_MARKER_END = "\uFFFB";
|
|
@@ -1157,6 +1559,7 @@ export {
|
|
|
1157
1559
|
readConfigOverride,
|
|
1158
1560
|
readExpOverride,
|
|
1159
1561
|
readGateOverride,
|
|
1562
|
+
see,
|
|
1160
1563
|
shipeasy,
|
|
1161
1564
|
version
|
|
1162
1565
|
};
|