@shipeasy/sdk 6.2.0 → 7.0.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 +1 -4
- package/dist/client/index.d.mts +147 -22
- package/dist/client/index.d.ts +147 -22
- package/dist/client/index.js +304 -125
- package/dist/client/index.mjs +303 -125
- package/dist/openfeature-server/index.d.mts +129 -8
- package/dist/openfeature-server/index.d.ts +129 -8
- package/dist/openfeature-server/index.js +66 -1
- package/dist/openfeature-server/index.mjs +66 -1
- package/dist/openfeature-web/index.d.mts +99 -27
- package/dist/openfeature-web/index.d.ts +99 -27
- package/dist/server/index.d.mts +166 -21
- package/dist/server/index.d.ts +166 -21
- package/dist/server/index.js +358 -131
- package/dist/server/index.mjs +357 -131
- package/docs/skill/SKILL.md +29 -25
- package/package.json +9 -6
package/dist/client/index.js
CHANGED
|
@@ -27,6 +27,7 @@ __export(client_exports, {
|
|
|
27
27
|
LABEL_MARKER_RE: () => LABEL_MARKER_RE,
|
|
28
28
|
LABEL_MARKER_SEP: () => LABEL_MARKER_SEP,
|
|
29
29
|
LABEL_MARKER_START: () => LABEL_MARKER_START,
|
|
30
|
+
LOG_LEVELS: () => LOG_LEVELS,
|
|
30
31
|
_resetConfigureForTests: () => _resetConfigureForTests,
|
|
31
32
|
_resetShipeasyForTests: () => _resetShipeasyForTests,
|
|
32
33
|
attachDevtools: () => attachDevtools,
|
|
@@ -226,7 +227,7 @@ function captureCallsiteStack() {
|
|
|
226
227
|
const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
|
|
227
228
|
return kept.length ? kept.join("\n") : void 0;
|
|
228
229
|
}
|
|
229
|
-
function buildSeeEvent(problem, consequence, extras,
|
|
230
|
+
function buildSeeEvent(problem, consequence, extras, ctx2, kindOverride, correlationId) {
|
|
230
231
|
let errorType;
|
|
231
232
|
let message;
|
|
232
233
|
let stack;
|
|
@@ -254,8 +255,8 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlat
|
|
|
254
255
|
message: truncate(message, SEE_MAX_MESSAGE),
|
|
255
256
|
subject: consequence.subject,
|
|
256
257
|
outcome: consequence.outcome,
|
|
257
|
-
side:
|
|
258
|
-
sdk_version:
|
|
258
|
+
side: ctx2.side,
|
|
259
|
+
sdk_version: ctx2.sdkVersion,
|
|
259
260
|
ts: Date.now()
|
|
260
261
|
};
|
|
261
262
|
if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
|
|
@@ -264,10 +265,10 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlat
|
|
|
264
265
|
if (causedBy) ev.caused_by = causedBy;
|
|
265
266
|
const cleanExtras = sanitizeExtras(extras);
|
|
266
267
|
if (cleanExtras) ev.extras = cleanExtras;
|
|
267
|
-
if (
|
|
268
|
-
if (
|
|
269
|
-
if (
|
|
270
|
-
if (
|
|
268
|
+
if (ctx2.url) ev.url = truncate(ctx2.url, SEE_MAX_SUBJECT);
|
|
269
|
+
if (ctx2.userId) ev.user_id = ctx2.userId;
|
|
270
|
+
if (ctx2.anonId) ev.anonymous_id = ctx2.anonId;
|
|
271
|
+
if (ctx2.env) ev.env = ctx2.env;
|
|
271
272
|
markReported(problem, ev);
|
|
272
273
|
return ev;
|
|
273
274
|
}
|
|
@@ -360,6 +361,98 @@ var SeeLimiter = class {
|
|
|
360
361
|
}
|
|
361
362
|
};
|
|
362
363
|
|
|
364
|
+
// src/internal-report.ts
|
|
365
|
+
var INGEST_URL = "https://api.shipeasy.ai/collect";
|
|
366
|
+
var PLACEHOLDER_KEY = "sdk_client_REPLACE_WITH_SHIPEASY_INTERNAL_ERROR_KEY";
|
|
367
|
+
var INGEST_KEY = "sdk_client_00bd4608a03e4084922978f9522614d5";
|
|
368
|
+
function keyConfigured() {
|
|
369
|
+
return !!INGEST_KEY && INGEST_KEY !== PLACEHOLDER_KEY;
|
|
370
|
+
}
|
|
371
|
+
var OUTCOME = "returned a safe default";
|
|
372
|
+
var SDK_ID = "ts";
|
|
373
|
+
var ctx = null;
|
|
374
|
+
var limiter = new SeeLimiter();
|
|
375
|
+
function setInternalReportContext(c) {
|
|
376
|
+
ctx = { side: c.side, sdkVersion: c.sdkVersion, enabled: c.enabled !== false };
|
|
377
|
+
}
|
|
378
|
+
function reportInternalError(label, err) {
|
|
379
|
+
try {
|
|
380
|
+
if (!ctx || !ctx.enabled || !keyConfigured()) return;
|
|
381
|
+
const ev = buildSeeEvent(
|
|
382
|
+
err,
|
|
383
|
+
causesThe(label).to(OUTCOME),
|
|
384
|
+
{ sdk: SDK_ID },
|
|
385
|
+
{ side: ctx.side, sdkVersion: ctx.sdkVersion },
|
|
386
|
+
"caught"
|
|
387
|
+
);
|
|
388
|
+
if (!limiter.shouldSend(ev)) return;
|
|
389
|
+
send2(INGEST_URL, INGEST_KEY, JSON.stringify({ events: [ev] }));
|
|
390
|
+
} catch {
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function send2(url, key, body) {
|
|
394
|
+
try {
|
|
395
|
+
const f = globalThis.fetch;
|
|
396
|
+
if (typeof f !== "function") return;
|
|
397
|
+
void f(url, {
|
|
398
|
+
method: "POST",
|
|
399
|
+
headers: { "X-SDK-Key": key, "Content-Type": "text/plain" },
|
|
400
|
+
body,
|
|
401
|
+
keepalive: true
|
|
402
|
+
}).catch(() => {
|
|
403
|
+
});
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/logger.ts
|
|
409
|
+
var LOG_LEVELS = ["silent", "error", "warn", "info", "debug"];
|
|
410
|
+
var RANK = {
|
|
411
|
+
silent: 0,
|
|
412
|
+
error: 1,
|
|
413
|
+
warn: 2,
|
|
414
|
+
info: 3,
|
|
415
|
+
debug: 4
|
|
416
|
+
};
|
|
417
|
+
var currentLevel = "warn";
|
|
418
|
+
function setLogLevel(level) {
|
|
419
|
+
if (typeof level === "string" && Object.prototype.hasOwnProperty.call(RANK, level)) {
|
|
420
|
+
currentLevel = level;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function write(method, args) {
|
|
424
|
+
try {
|
|
425
|
+
const c = globalThis.console;
|
|
426
|
+
if (!c) return;
|
|
427
|
+
const fn = c[method] ?? c.log;
|
|
428
|
+
if (typeof fn === "function") fn.call(c, ...args);
|
|
429
|
+
} catch {
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
var logger = {
|
|
433
|
+
error(...args) {
|
|
434
|
+
if (RANK[currentLevel] >= RANK.error) write("error", args);
|
|
435
|
+
},
|
|
436
|
+
warn(...args) {
|
|
437
|
+
if (RANK[currentLevel] >= RANK.warn) write("warn", args);
|
|
438
|
+
},
|
|
439
|
+
info(...args) {
|
|
440
|
+
if (RANK[currentLevel] >= RANK.info) write("info", args);
|
|
441
|
+
},
|
|
442
|
+
debug(...args) {
|
|
443
|
+
if (RANK[currentLevel] >= RANK.debug) write("debug", args);
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
function safeRun(label, fallback, fn) {
|
|
447
|
+
try {
|
|
448
|
+
return fn();
|
|
449
|
+
} catch (err) {
|
|
450
|
+
logger.error(`[shipeasy] ${label} failed \u2014 returning safe default:`, String(err));
|
|
451
|
+
reportInternalError(label, err);
|
|
452
|
+
return fallback;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
363
456
|
// src/client/index.ts
|
|
364
457
|
var version = "4.0.0";
|
|
365
458
|
var FLAG_REASONS = [
|
|
@@ -370,6 +463,40 @@ var FLAG_REASONS = [
|
|
|
370
463
|
"RULE_MATCH",
|
|
371
464
|
"DEFAULT"
|
|
372
465
|
];
|
|
466
|
+
var AssignmentImpl = class {
|
|
467
|
+
constructor(name, group, params) {
|
|
468
|
+
this.name = name;
|
|
469
|
+
this.group = group;
|
|
470
|
+
this.params = params;
|
|
471
|
+
}
|
|
472
|
+
name;
|
|
473
|
+
group;
|
|
474
|
+
params;
|
|
475
|
+
get enrolled() {
|
|
476
|
+
return this.group !== null;
|
|
477
|
+
}
|
|
478
|
+
get(field, fallback) {
|
|
479
|
+
const v = this.params[field];
|
|
480
|
+
return v === void 0 ? fallback : v;
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
function hasDomEvents() {
|
|
484
|
+
return typeof window !== "undefined" && typeof window.addEventListener === "function";
|
|
485
|
+
}
|
|
486
|
+
function onWindow(type, handler, opts) {
|
|
487
|
+
try {
|
|
488
|
+
if (hasDomEvents()) window.addEventListener(type, handler, opts);
|
|
489
|
+
} catch {
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function onDocument(type, handler, opts) {
|
|
493
|
+
try {
|
|
494
|
+
if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
|
|
495
|
+
document.addEventListener(type, handler, opts);
|
|
496
|
+
}
|
|
497
|
+
} catch {
|
|
498
|
+
}
|
|
499
|
+
}
|
|
373
500
|
var FLUSH_INTERVAL_MS = 5e3;
|
|
374
501
|
var MAX_BUFFER = 100;
|
|
375
502
|
var ANON_ID_KEY = "__se_anon_id";
|
|
@@ -381,8 +508,8 @@ var EventBuffer = class {
|
|
|
381
508
|
this.sdkKey = sdkKey;
|
|
382
509
|
if (typeof window !== "undefined") {
|
|
383
510
|
this.timer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
|
|
384
|
-
|
|
385
|
-
|
|
511
|
+
onWindow("beforeunload", () => this.flush());
|
|
512
|
+
onDocument("visibilitychange", () => {
|
|
386
513
|
if (document.visibilityState === "hidden") this.flush(true);
|
|
387
514
|
});
|
|
388
515
|
try {
|
|
@@ -927,6 +1054,10 @@ var Engine = class _Engine {
|
|
|
927
1054
|
env;
|
|
928
1055
|
evalResult = null;
|
|
929
1056
|
anonId;
|
|
1057
|
+
// Resolves once a caller-supplied anonymousStore has been consulted (persisted
|
|
1058
|
+
// id adopted, or the minted id written back). identify() awaits it so the
|
|
1059
|
+
// stable id is settled before the first /sdk/evaluate. null when no store.
|
|
1060
|
+
anonReady = null;
|
|
930
1061
|
userId = "";
|
|
931
1062
|
buffer;
|
|
932
1063
|
telemetry;
|
|
@@ -952,10 +1083,16 @@ var Engine = class _Engine {
|
|
|
952
1083
|
this.notify();
|
|
953
1084
|
};
|
|
954
1085
|
constructor(opts) {
|
|
1086
|
+
setLogLevel(opts.logLevel);
|
|
955
1087
|
this.sdkKey = opts.sdkKey;
|
|
956
|
-
this.baseUrl = (opts.baseUrl ?? "https://
|
|
1088
|
+
this.baseUrl = (opts.baseUrl ?? "https://api.shipeasy.ai").replace(/\/$/, "");
|
|
957
1089
|
this.env = opts.env ?? "prod";
|
|
958
1090
|
this.testMode = opts.testMode === true;
|
|
1091
|
+
setInternalReportContext({
|
|
1092
|
+
side: "client",
|
|
1093
|
+
sdkVersion: version,
|
|
1094
|
+
enabled: !this.testMode && opts.disableInternalErrorReporting !== true
|
|
1095
|
+
});
|
|
959
1096
|
this.autoGuardrails = opts.autoGuardrails !== false;
|
|
960
1097
|
this.autoCollectAlways = opts.autoCollectAlways === true;
|
|
961
1098
|
this.disableAutoExposure = opts.disableAutoExposure === true;
|
|
@@ -968,6 +1105,9 @@ var Engine = class _Engine {
|
|
|
968
1105
|
engagement: g.engagement ?? this.autoGuardrails
|
|
969
1106
|
};
|
|
970
1107
|
this.anonId = getOrCreateAnonId();
|
|
1108
|
+
if (opts.anonymousStore && !this.testMode) {
|
|
1109
|
+
this.anonReady = this.hydrateAnonFromStore(opts.anonymousStore);
|
|
1110
|
+
}
|
|
971
1111
|
this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
|
|
972
1112
|
this.telemetry = new Telemetry({
|
|
973
1113
|
endpoint: opts.telemetryUrl ?? DEFAULT_TELEMETRY_URL,
|
|
@@ -983,6 +1123,23 @@ var Engine = class _Engine {
|
|
|
983
1123
|
void this.buffer.flushPendingAlias();
|
|
984
1124
|
}
|
|
985
1125
|
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Consult the caller-supplied {@link AnonymousStore}: adopt a persisted id so
|
|
1128
|
+
* the visitor buckets identically across app launches, or persist the id just
|
|
1129
|
+
* minted by getOrCreateAnonId() on a fresh device. Best-effort — any store
|
|
1130
|
+
* failure leaves the in-memory id in place.
|
|
1131
|
+
*/
|
|
1132
|
+
async hydrateAnonFromStore(store) {
|
|
1133
|
+
try {
|
|
1134
|
+
const stored = await store.get(ANON_ID_KEY);
|
|
1135
|
+
if (typeof stored === "string" && stored) {
|
|
1136
|
+
this.anonId = stored;
|
|
1137
|
+
} else {
|
|
1138
|
+
await store.set(ANON_ID_KEY, this.anonId);
|
|
1139
|
+
}
|
|
1140
|
+
} catch {
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
986
1143
|
/**
|
|
987
1144
|
* Build a no-network, immediately-usable browser client for tests
|
|
988
1145
|
* (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
|
|
@@ -1010,6 +1167,10 @@ var Engine = class _Engine {
|
|
|
1010
1167
|
this.notify();
|
|
1011
1168
|
return;
|
|
1012
1169
|
}
|
|
1170
|
+
if (this.anonReady) {
|
|
1171
|
+
await this.anonReady;
|
|
1172
|
+
this.anonReady = null;
|
|
1173
|
+
}
|
|
1013
1174
|
const seq = ++this.identifySeq;
|
|
1014
1175
|
const prevUserId = this.userId;
|
|
1015
1176
|
if (user.user_id !== void 0) this.userId = user.user_id;
|
|
@@ -1086,7 +1247,7 @@ var Engine = class _Engine {
|
|
|
1086
1247
|
try {
|
|
1087
1248
|
l();
|
|
1088
1249
|
} catch (err) {
|
|
1089
|
-
|
|
1250
|
+
logger.warn("[shipeasy] subscriber threw:", String(err));
|
|
1090
1251
|
}
|
|
1091
1252
|
}
|
|
1092
1253
|
}
|
|
@@ -1163,54 +1324,41 @@ var Engine = class _Engine {
|
|
|
1163
1324
|
try {
|
|
1164
1325
|
return opts.decode(raw);
|
|
1165
1326
|
} catch (err) {
|
|
1166
|
-
|
|
1327
|
+
logger.warn(`[shipeasy] getConfig('${name}') decode failed:`, String(err));
|
|
1167
1328
|
return void 0;
|
|
1168
1329
|
}
|
|
1169
1330
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1331
|
+
/**
|
|
1332
|
+
* Assign the visitor within a universe: `universe("checkout").assign()`. A
|
|
1333
|
+
* universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
|
|
1334
|
+
* the returned {@link Assignment} exposes `.group` / `.get(field, fallback)`
|
|
1335
|
+
* and auto-logs one exposure when enrolled (unless `disableAutoExposure` or
|
|
1336
|
+
* `assign({ logExposure: false })`). An un-enrolled visitor still resolves
|
|
1337
|
+
* `get()` to the universe defaults. This is the sole experiment read path —
|
|
1338
|
+
* there is no `getExperiment` (ask a universe, not an experiment).
|
|
1339
|
+
*/
|
|
1340
|
+
universe(name) {
|
|
1341
|
+
return { assign: (opts) => this.assignUniverse(name, opts) };
|
|
1342
|
+
}
|
|
1343
|
+
assignUniverse(name, opts) {
|
|
1173
1344
|
this.telemetry.emit("experiment", name);
|
|
1174
|
-
const
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
params
|
|
1178
|
-
};
|
|
1179
|
-
const pov = this.experimentOverrides.get(name);
|
|
1180
|
-
if (pov) {
|
|
1181
|
-
const variantParams = variants?.[pov.group];
|
|
1182
|
-
const params = { ...defaultParams, ...pov.params, ...variantParams ?? {} };
|
|
1183
|
-
return { inExperiment: true, group: pov.group, params };
|
|
1345
|
+
const defaults = this.evalResult?.universes?.[name]?.defaults ?? {};
|
|
1346
|
+
for (const [expName, pov] of this.experimentOverrides) {
|
|
1347
|
+
if (this.evalResult?.experiments[expName]?.universe !== name) continue;
|
|
1348
|
+
return new AssignmentImpl(expName, pov.group, { ...defaults, ...pov.params });
|
|
1184
1349
|
}
|
|
1185
|
-
const
|
|
1186
|
-
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1189
|
-
return { inExperiment: true, group: ov, params };
|
|
1350
|
+
for (const [expName, entry] of Object.entries(this.evalResult?.experiments ?? {})) {
|
|
1351
|
+
if (entry.universe !== name) continue;
|
|
1352
|
+
const ov = readExpOverride(expName);
|
|
1353
|
+
if (ov !== null) return new AssignmentImpl(expName, ov, { ...defaults, ...entry.params });
|
|
1190
1354
|
}
|
|
1191
|
-
const entry
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
try {
|
|
1197
|
-
return { inExperiment: true, group: entry.group, params: decode(entry.params) };
|
|
1198
|
-
} catch (err) {
|
|
1199
|
-
console.warn(`[shipeasy] getExperiment('${name}') decode failed:`, String(err));
|
|
1200
|
-
return notIn;
|
|
1355
|
+
for (const [expName, entry] of Object.entries(this.evalResult?.experiments ?? {})) {
|
|
1356
|
+
if (entry.universe !== name || !entry.inExperiment) continue;
|
|
1357
|
+
const shouldLog = opts?.logExposure ?? !this.disableAutoExposure;
|
|
1358
|
+
if (shouldLog) this.buffer.pushExposure(expName, entry.group, this.userId, this.anonId);
|
|
1359
|
+
return new AssignmentImpl(expName, entry.group, entry.params);
|
|
1201
1360
|
}
|
|
1202
|
-
|
|
1203
|
-
/**
|
|
1204
|
-
* Manually log an exposure for an enrolled experiment (Statsig's
|
|
1205
|
-
* `manuallyLogExposure`). Reads the cached eval result; if the visitor is in
|
|
1206
|
-
* the experiment, pushes the session-deduped exposure. Pair this with the
|
|
1207
|
-
* render of the treatment when reading with `{ logExposure: false }` (or
|
|
1208
|
-
* `disableAutoExposure: true`). No-op if the visitor isn't enrolled.
|
|
1209
|
-
*/
|
|
1210
|
-
logExposure(name) {
|
|
1211
|
-
const entry = this.evalResult?.experiments[name];
|
|
1212
|
-
if (!entry || !entry.inExperiment) return;
|
|
1213
|
-
this.buffer.pushExposure(name, entry.group, this.userId, this.anonId);
|
|
1361
|
+
return new AssignmentImpl(null, null, defaults);
|
|
1214
1362
|
}
|
|
1215
1363
|
/**
|
|
1216
1364
|
* Subscribe to state changes — fires after identify() completes and on
|
|
@@ -1219,7 +1367,7 @@ var Engine = class _Engine {
|
|
|
1219
1367
|
*/
|
|
1220
1368
|
subscribe(listener) {
|
|
1221
1369
|
this.listeners.add(listener);
|
|
1222
|
-
if (!this.overrideListenerInstalled &&
|
|
1370
|
+
if (!this.overrideListenerInstalled && hasDomEvents()) {
|
|
1223
1371
|
this.overrideListenerInstalled = true;
|
|
1224
1372
|
window.addEventListener("se:override:change", this.onOverrideChange);
|
|
1225
1373
|
}
|
|
@@ -1235,9 +1383,11 @@ var Engine = class _Engine {
|
|
|
1235
1383
|
if (typeof window === "undefined") return null;
|
|
1236
1384
|
const bridge = {
|
|
1237
1385
|
getFlag: (n) => this.getFlag(n),
|
|
1386
|
+
// Devtools reads assignments by experiment name straight off the eval
|
|
1387
|
+
// result (the public read path is universe-first; this is display-only).
|
|
1238
1388
|
getExperiment: (n) => {
|
|
1239
|
-
const
|
|
1240
|
-
return { inExperiment:
|
|
1389
|
+
const entry = this.evalResult?.experiments[n];
|
|
1390
|
+
return { inExperiment: entry?.inExperiment ?? false, group: entry?.group ?? "control" };
|
|
1241
1391
|
},
|
|
1242
1392
|
getConfig: (n) => this.getConfig(n)
|
|
1243
1393
|
};
|
|
@@ -1360,7 +1510,7 @@ function loadDevtools(opts = {}) {
|
|
|
1360
1510
|
}
|
|
1361
1511
|
}
|
|
1362
1512
|
function attachDevtools(client, opts = {}) {
|
|
1363
|
-
if (
|
|
1513
|
+
if (!hasDomEvents()) return () => {
|
|
1364
1514
|
};
|
|
1365
1515
|
const hotkey = opts.hotkey ?? "Shift+Alt+S";
|
|
1366
1516
|
const parts = hotkey.split("+");
|
|
@@ -1399,19 +1549,22 @@ function shipeasy(opts) {
|
|
|
1399
1549
|
const client = configureShipeasy({
|
|
1400
1550
|
sdkKey: opts.clientKey,
|
|
1401
1551
|
baseUrl,
|
|
1552
|
+
logLevel: opts.logLevel,
|
|
1402
1553
|
autoGuardrails: blanket,
|
|
1403
1554
|
autoGuardrailGroups: groups,
|
|
1404
1555
|
autoCollectAlways: acObj?.always === true,
|
|
1405
1556
|
disableTelemetry: opts.disableTelemetry,
|
|
1557
|
+
disableInternalErrorReporting: opts.disableInternalErrorReporting,
|
|
1406
1558
|
disableAutoExposure: opts.disableAutoExposure,
|
|
1407
1559
|
privateAttributes: opts.privateAttributes,
|
|
1408
|
-
stickyBucketing: opts.stickyBucketing
|
|
1560
|
+
stickyBucketing: opts.stickyBucketing,
|
|
1561
|
+
anonymousStore: opts.anonymousStore
|
|
1409
1562
|
});
|
|
1410
1563
|
injectI18nLoader(opts.clientKey, baseUrl, opts.i18nProfile);
|
|
1411
1564
|
flags.notifyMounted();
|
|
1412
1565
|
if (opts.autoIdentify !== false) {
|
|
1413
1566
|
void client.identify({}).catch((err) => {
|
|
1414
|
-
|
|
1567
|
+
logger.warn("[shipeasy] auto-identify failed:", String(err));
|
|
1415
1568
|
});
|
|
1416
1569
|
}
|
|
1417
1570
|
return attachDevtools(client, { adminUrl: opts.adminUrl });
|
|
@@ -1487,7 +1640,7 @@ var _mountedAndReady = false;
|
|
|
1487
1640
|
var _standaloneListeners = /* @__PURE__ */ new Set();
|
|
1488
1641
|
var _standaloneOverrideWired = false;
|
|
1489
1642
|
function wireStandaloneOverride() {
|
|
1490
|
-
if (_standaloneOverrideWired ||
|
|
1643
|
+
if (_standaloneOverrideWired || !hasDomEvents()) return;
|
|
1491
1644
|
_standaloneOverrideWired = true;
|
|
1492
1645
|
window.addEventListener("se:override:change", () => {
|
|
1493
1646
|
for (const cb of _standaloneListeners) cb();
|
|
@@ -1499,7 +1652,7 @@ var flags = {
|
|
|
1499
1652
|
},
|
|
1500
1653
|
identify(user) {
|
|
1501
1654
|
if (!_client) {
|
|
1502
|
-
|
|
1655
|
+
logger.warn("[shipeasy] flags.identify called before configureShipeasy()");
|
|
1503
1656
|
return Promise.resolve();
|
|
1504
1657
|
}
|
|
1505
1658
|
return _client.identify(user);
|
|
@@ -1512,57 +1665,66 @@ var flags = {
|
|
|
1512
1665
|
* force-static pages where SSR has no flag data.
|
|
1513
1666
|
*/
|
|
1514
1667
|
get(name, defaultValue = false) {
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1668
|
+
return safeRun("flags.get", defaultValue, () => {
|
|
1669
|
+
const bs = getBootstrap();
|
|
1670
|
+
if (bs !== null && name in bs.flags) return bs.flags[name];
|
|
1671
|
+
if (!_mountedAndReady) return defaultValue;
|
|
1672
|
+
if (_client) return _client.getFlag(name, defaultValue);
|
|
1673
|
+
return readGateOverride(name) ?? defaultValue;
|
|
1674
|
+
});
|
|
1520
1675
|
},
|
|
1521
1676
|
/** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
|
|
1522
1677
|
getDetail(name) {
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1678
|
+
return safeRun("flags.getDetail", { value: false, reason: "CLIENT_NOT_READY" }, () => {
|
|
1679
|
+
if (_client) return _client.getFlagDetail(name);
|
|
1680
|
+
const ov = readGateOverride(name);
|
|
1681
|
+
if (ov !== null) return { value: ov, reason: "OVERRIDE" };
|
|
1682
|
+
return { value: false, reason: "CLIENT_NOT_READY" };
|
|
1683
|
+
});
|
|
1527
1684
|
},
|
|
1528
1685
|
getConfig(name, decode) {
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1686
|
+
return safeRun("flags.getConfig", void 0, () => {
|
|
1687
|
+
const bs = getBootstrap();
|
|
1688
|
+
if (bs !== null && name in bs.configs) {
|
|
1689
|
+
const raw = bs.configs[name];
|
|
1690
|
+
if (!decode) return raw;
|
|
1691
|
+
try {
|
|
1692
|
+
return decode(raw);
|
|
1693
|
+
} catch {
|
|
1694
|
+
return void 0;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
if (!_mountedAndReady) return void 0;
|
|
1698
|
+
if (_client) return _client.getConfig(name, decode);
|
|
1699
|
+
const ov = readConfigOverride(name);
|
|
1700
|
+
if (ov === void 0) return void 0;
|
|
1701
|
+
if (!decode) return ov;
|
|
1533
1702
|
try {
|
|
1534
|
-
return decode(
|
|
1703
|
+
return decode(ov);
|
|
1535
1704
|
} catch {
|
|
1536
1705
|
return void 0;
|
|
1537
1706
|
}
|
|
1538
|
-
}
|
|
1539
|
-
if (!_mountedAndReady) return void 0;
|
|
1540
|
-
if (_client) return _client.getConfig(name, decode);
|
|
1541
|
-
const ov = readConfigOverride(name);
|
|
1542
|
-
if (ov === void 0) return void 0;
|
|
1543
|
-
if (!decode) return ov;
|
|
1544
|
-
try {
|
|
1545
|
-
return decode(ov);
|
|
1546
|
-
} catch {
|
|
1547
|
-
return void 0;
|
|
1548
|
-
}
|
|
1707
|
+
});
|
|
1549
1708
|
},
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1709
|
+
/**
|
|
1710
|
+
* Assign the visitor within a universe: `flags.universe("checkout").assign()`.
|
|
1711
|
+
* A universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
|
|
1712
|
+
* the returned {@link Assignment} exposes `.group` / `.get(field, fallback)`
|
|
1713
|
+
* and auto-logs one exposure when enrolled. Before configure() (or on error)
|
|
1714
|
+
* returns a safe not-enrolled handle. Replaces the removed `getExperiment` —
|
|
1715
|
+
* read experiments by universe, never by name.
|
|
1716
|
+
*/
|
|
1717
|
+
universe(name) {
|
|
1718
|
+
return {
|
|
1719
|
+
assign: (opts) => safeRun(
|
|
1720
|
+
"flags.universe.assign",
|
|
1721
|
+
new AssignmentImpl(null, null, {}),
|
|
1722
|
+
() => _client ? _client.universe(name).assign(opts) : new AssignmentImpl(null, null, {})
|
|
1723
|
+
)
|
|
1555
1724
|
};
|
|
1556
|
-
if (!_client) return fallback;
|
|
1557
|
-
return typeof decodeOrOpts === "function" ? _client.getExperiment(name, defaultParams, decodeOrOpts, variants) : _client.getExperiment(name, defaultParams, decodeOrOpts ?? {});
|
|
1558
|
-
},
|
|
1559
|
-
/** Manually log an exposure for an enrolled experiment. See
|
|
1560
|
-
* {@link Engine.logExposure}. No-op before configure(). */
|
|
1561
|
-
logExposure(name) {
|
|
1562
|
-
_client?.logExposure(name);
|
|
1563
1725
|
},
|
|
1564
1726
|
track(eventName, props) {
|
|
1565
|
-
_client?.track(eventName, props);
|
|
1727
|
+
safeRun("flags.track", void 0, () => _client?.track(eventName, props));
|
|
1566
1728
|
},
|
|
1567
1729
|
/**
|
|
1568
1730
|
* Read a killswitch. Without `switchKey`, returns true when the killswitch is
|
|
@@ -1574,15 +1736,17 @@ var flags = {
|
|
|
1574
1736
|
* available synchronously on first render.
|
|
1575
1737
|
*/
|
|
1576
1738
|
ks(name, switchKey) {
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1739
|
+
return safeRun("flags.ks", false, () => {
|
|
1740
|
+
const bs = getBootstrap();
|
|
1741
|
+
if (bs !== null && bs.killswitches && name in bs.killswitches) {
|
|
1742
|
+
const ks = bs.killswitches[name];
|
|
1743
|
+
if (typeof ks === "boolean") return switchKey === void 0 ? ks : false;
|
|
1744
|
+
if (switchKey === void 0) return false;
|
|
1745
|
+
return ks[switchKey] === true;
|
|
1746
|
+
}
|
|
1747
|
+
if (!_mountedAndReady) return false;
|
|
1748
|
+
return _client?.getKillswitch(name, switchKey) ?? false;
|
|
1749
|
+
});
|
|
1586
1750
|
},
|
|
1587
1751
|
flush() {
|
|
1588
1752
|
return _client?.flush() ?? Promise.resolve();
|
|
@@ -1639,7 +1803,7 @@ var Client = class {
|
|
|
1639
1803
|
this.engine = engine;
|
|
1640
1804
|
this.attributes = _attributes(user);
|
|
1641
1805
|
this._identify = this.engine.identify(this.attributes).catch((err) => {
|
|
1642
|
-
|
|
1806
|
+
logger.warn("[shipeasy] Client identify failed:", String(err));
|
|
1643
1807
|
});
|
|
1644
1808
|
}
|
|
1645
1809
|
/** Resolves once the engine's identify() for this user has completed. */
|
|
@@ -1647,20 +1811,40 @@ var Client = class {
|
|
|
1647
1811
|
return this._identify;
|
|
1648
1812
|
}
|
|
1649
1813
|
getFlag(name, defaultValue = false) {
|
|
1650
|
-
return this.engine.getFlag(name, defaultValue);
|
|
1814
|
+
return safeRun("Client.getFlag", defaultValue, () => this.engine.getFlag(name, defaultValue));
|
|
1651
1815
|
}
|
|
1652
1816
|
getFlagDetail(name) {
|
|
1653
|
-
return
|
|
1817
|
+
return safeRun(
|
|
1818
|
+
"Client.getFlagDetail",
|
|
1819
|
+
{ value: false, reason: "CLIENT_NOT_READY" },
|
|
1820
|
+
() => this.engine.getFlagDetail(name)
|
|
1821
|
+
);
|
|
1654
1822
|
}
|
|
1655
1823
|
getConfig(name, decodeOrOpts) {
|
|
1656
|
-
return
|
|
1824
|
+
return safeRun(
|
|
1825
|
+
"Client.getConfig",
|
|
1826
|
+
void 0,
|
|
1827
|
+
() => this.engine.getConfig(name, decodeOrOpts)
|
|
1828
|
+
);
|
|
1657
1829
|
}
|
|
1658
|
-
|
|
1659
|
-
|
|
1830
|
+
/**
|
|
1831
|
+
* Assign the visitor within a universe: `client.universe("checkout").assign()`.
|
|
1832
|
+
* A universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
|
|
1833
|
+
* returns an {@link Assignment} (`.group` / `.get(field, fallback)`) and
|
|
1834
|
+
* auto-logs one exposure when enrolled. Replaces the removed `getExperiment`.
|
|
1835
|
+
*/
|
|
1836
|
+
universe(name) {
|
|
1837
|
+
return {
|
|
1838
|
+
assign: (opts) => safeRun(
|
|
1839
|
+
"Client.universe.assign",
|
|
1840
|
+
new AssignmentImpl(null, null, {}),
|
|
1841
|
+
() => this.engine.universe(name).assign(opts)
|
|
1842
|
+
)
|
|
1843
|
+
};
|
|
1660
1844
|
}
|
|
1661
1845
|
/** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
|
|
1662
1846
|
getKillswitch(name, switchKey) {
|
|
1663
|
-
return this.engine.getKillswitch(name, switchKey);
|
|
1847
|
+
return safeRun("Client.getKillswitch", false, () => this.engine.getKillswitch(name, switchKey));
|
|
1664
1848
|
}
|
|
1665
1849
|
/**
|
|
1666
1850
|
* Record a conversion/metric event for the bound (identified) user. Delegates
|
|
@@ -1669,21 +1853,12 @@ var Client = class {
|
|
|
1669
1853
|
* test mode.
|
|
1670
1854
|
*/
|
|
1671
1855
|
track(eventName, props) {
|
|
1672
|
-
this.engine.track(eventName, props);
|
|
1673
|
-
}
|
|
1674
|
-
/**
|
|
1675
|
-
* Log an exposure for `name` at the treatment's render for the bound user.
|
|
1676
|
-
* Delegates to {@link Engine.logExposure} (no-op when the visitor isn't
|
|
1677
|
-
* enrolled). Pair with `getExperiment(name, …, { logExposure: false })` /
|
|
1678
|
-
* `disableAutoExposure` to log exposure exactly when you render.
|
|
1679
|
-
*/
|
|
1680
|
-
logExposure(name) {
|
|
1681
|
-
this.engine.logExposure(name);
|
|
1856
|
+
safeRun("Client.track", void 0, () => this.engine.track(eventName, props));
|
|
1682
1857
|
}
|
|
1683
1858
|
};
|
|
1684
1859
|
function dispatchSee(problem, consequence, extras, kind) {
|
|
1685
1860
|
if (!_client) {
|
|
1686
|
-
|
|
1861
|
+
logger.warn("[shipeasy] see() called before shipeasy({ clientKey }) \u2014 error dropped");
|
|
1687
1862
|
return;
|
|
1688
1863
|
}
|
|
1689
1864
|
_client.reportError(problem, consequence, extras, kind);
|
|
@@ -1905,6 +2080,7 @@ var i18n = {
|
|
|
1905
2080
|
whenReady() {
|
|
1906
2081
|
if (typeof window === "undefined") return Promise.resolve();
|
|
1907
2082
|
if (window.i18n?.locale) return Promise.resolve();
|
|
2083
|
+
if (!hasDomEvents()) return Promise.resolve();
|
|
1908
2084
|
return new Promise((resolve) => {
|
|
1909
2085
|
const handler = () => resolve();
|
|
1910
2086
|
window.addEventListener("se:i18n:ready", handler, { once: true });
|
|
@@ -1915,6 +2091,8 @@ var i18n = {
|
|
|
1915
2091
|
if (typeof window === "undefined") return () => {
|
|
1916
2092
|
};
|
|
1917
2093
|
if (window.i18n) return window.i18n.on("update", cb);
|
|
2094
|
+
if (!hasDomEvents()) return () => {
|
|
2095
|
+
};
|
|
1918
2096
|
let unsub = () => {
|
|
1919
2097
|
};
|
|
1920
2098
|
const handler = () => {
|
|
@@ -1936,6 +2114,7 @@ var i18n = {
|
|
|
1936
2114
|
LABEL_MARKER_RE,
|
|
1937
2115
|
LABEL_MARKER_SEP,
|
|
1938
2116
|
LABEL_MARKER_START,
|
|
2117
|
+
LOG_LEVELS,
|
|
1939
2118
|
_resetConfigureForTests,
|
|
1940
2119
|
_resetShipeasyForTests,
|
|
1941
2120
|
attachDevtools,
|