@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.mjs
CHANGED
|
@@ -174,7 +174,7 @@ function captureCallsiteStack() {
|
|
|
174
174
|
const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
|
|
175
175
|
return kept.length ? kept.join("\n") : void 0;
|
|
176
176
|
}
|
|
177
|
-
function buildSeeEvent(problem, consequence, extras,
|
|
177
|
+
function buildSeeEvent(problem, consequence, extras, ctx2, kindOverride, correlationId) {
|
|
178
178
|
let errorType;
|
|
179
179
|
let message;
|
|
180
180
|
let stack;
|
|
@@ -202,8 +202,8 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlat
|
|
|
202
202
|
message: truncate(message, SEE_MAX_MESSAGE),
|
|
203
203
|
subject: consequence.subject,
|
|
204
204
|
outcome: consequence.outcome,
|
|
205
|
-
side:
|
|
206
|
-
sdk_version:
|
|
205
|
+
side: ctx2.side,
|
|
206
|
+
sdk_version: ctx2.sdkVersion,
|
|
207
207
|
ts: Date.now()
|
|
208
208
|
};
|
|
209
209
|
if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
|
|
@@ -212,10 +212,10 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlat
|
|
|
212
212
|
if (causedBy) ev.caused_by = causedBy;
|
|
213
213
|
const cleanExtras = sanitizeExtras(extras);
|
|
214
214
|
if (cleanExtras) ev.extras = cleanExtras;
|
|
215
|
-
if (
|
|
216
|
-
if (
|
|
217
|
-
if (
|
|
218
|
-
if (
|
|
215
|
+
if (ctx2.url) ev.url = truncate(ctx2.url, SEE_MAX_SUBJECT);
|
|
216
|
+
if (ctx2.userId) ev.user_id = ctx2.userId;
|
|
217
|
+
if (ctx2.anonId) ev.anonymous_id = ctx2.anonId;
|
|
218
|
+
if (ctx2.env) ev.env = ctx2.env;
|
|
219
219
|
markReported(problem, ev);
|
|
220
220
|
return ev;
|
|
221
221
|
}
|
|
@@ -308,6 +308,98 @@ var SeeLimiter = class {
|
|
|
308
308
|
}
|
|
309
309
|
};
|
|
310
310
|
|
|
311
|
+
// src/internal-report.ts
|
|
312
|
+
var INGEST_URL = "https://api.shipeasy.ai/collect";
|
|
313
|
+
var PLACEHOLDER_KEY = "sdk_client_REPLACE_WITH_SHIPEASY_INTERNAL_ERROR_KEY";
|
|
314
|
+
var INGEST_KEY = "sdk_client_00bd4608a03e4084922978f9522614d5";
|
|
315
|
+
function keyConfigured() {
|
|
316
|
+
return !!INGEST_KEY && INGEST_KEY !== PLACEHOLDER_KEY;
|
|
317
|
+
}
|
|
318
|
+
var OUTCOME = "returned a safe default";
|
|
319
|
+
var SDK_ID = "ts";
|
|
320
|
+
var ctx = null;
|
|
321
|
+
var limiter = new SeeLimiter();
|
|
322
|
+
function setInternalReportContext(c) {
|
|
323
|
+
ctx = { side: c.side, sdkVersion: c.sdkVersion, enabled: c.enabled !== false };
|
|
324
|
+
}
|
|
325
|
+
function reportInternalError(label, err) {
|
|
326
|
+
try {
|
|
327
|
+
if (!ctx || !ctx.enabled || !keyConfigured()) return;
|
|
328
|
+
const ev = buildSeeEvent(
|
|
329
|
+
err,
|
|
330
|
+
causesThe(label).to(OUTCOME),
|
|
331
|
+
{ sdk: SDK_ID },
|
|
332
|
+
{ side: ctx.side, sdkVersion: ctx.sdkVersion },
|
|
333
|
+
"caught"
|
|
334
|
+
);
|
|
335
|
+
if (!limiter.shouldSend(ev)) return;
|
|
336
|
+
send2(INGEST_URL, INGEST_KEY, JSON.stringify({ events: [ev] }));
|
|
337
|
+
} catch {
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function send2(url, key, body) {
|
|
341
|
+
try {
|
|
342
|
+
const f = globalThis.fetch;
|
|
343
|
+
if (typeof f !== "function") return;
|
|
344
|
+
void f(url, {
|
|
345
|
+
method: "POST",
|
|
346
|
+
headers: { "X-SDK-Key": key, "Content-Type": "text/plain" },
|
|
347
|
+
body,
|
|
348
|
+
keepalive: true
|
|
349
|
+
}).catch(() => {
|
|
350
|
+
});
|
|
351
|
+
} catch {
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/logger.ts
|
|
356
|
+
var LOG_LEVELS = ["silent", "error", "warn", "info", "debug"];
|
|
357
|
+
var RANK = {
|
|
358
|
+
silent: 0,
|
|
359
|
+
error: 1,
|
|
360
|
+
warn: 2,
|
|
361
|
+
info: 3,
|
|
362
|
+
debug: 4
|
|
363
|
+
};
|
|
364
|
+
var currentLevel = "warn";
|
|
365
|
+
function setLogLevel(level) {
|
|
366
|
+
if (typeof level === "string" && Object.prototype.hasOwnProperty.call(RANK, level)) {
|
|
367
|
+
currentLevel = level;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function write(method, args) {
|
|
371
|
+
try {
|
|
372
|
+
const c = globalThis.console;
|
|
373
|
+
if (!c) return;
|
|
374
|
+
const fn = c[method] ?? c.log;
|
|
375
|
+
if (typeof fn === "function") fn.call(c, ...args);
|
|
376
|
+
} catch {
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
var logger = {
|
|
380
|
+
error(...args) {
|
|
381
|
+
if (RANK[currentLevel] >= RANK.error) write("error", args);
|
|
382
|
+
},
|
|
383
|
+
warn(...args) {
|
|
384
|
+
if (RANK[currentLevel] >= RANK.warn) write("warn", args);
|
|
385
|
+
},
|
|
386
|
+
info(...args) {
|
|
387
|
+
if (RANK[currentLevel] >= RANK.info) write("info", args);
|
|
388
|
+
},
|
|
389
|
+
debug(...args) {
|
|
390
|
+
if (RANK[currentLevel] >= RANK.debug) write("debug", args);
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
function safeRun(label, fallback, fn) {
|
|
394
|
+
try {
|
|
395
|
+
return fn();
|
|
396
|
+
} catch (err) {
|
|
397
|
+
logger.error(`[shipeasy] ${label} failed \u2014 returning safe default:`, String(err));
|
|
398
|
+
reportInternalError(label, err);
|
|
399
|
+
return fallback;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
311
403
|
// src/client/index.ts
|
|
312
404
|
var version = "4.0.0";
|
|
313
405
|
var FLAG_REASONS = [
|
|
@@ -318,6 +410,40 @@ var FLAG_REASONS = [
|
|
|
318
410
|
"RULE_MATCH",
|
|
319
411
|
"DEFAULT"
|
|
320
412
|
];
|
|
413
|
+
var AssignmentImpl = class {
|
|
414
|
+
constructor(name, group, params) {
|
|
415
|
+
this.name = name;
|
|
416
|
+
this.group = group;
|
|
417
|
+
this.params = params;
|
|
418
|
+
}
|
|
419
|
+
name;
|
|
420
|
+
group;
|
|
421
|
+
params;
|
|
422
|
+
get enrolled() {
|
|
423
|
+
return this.group !== null;
|
|
424
|
+
}
|
|
425
|
+
get(field, fallback) {
|
|
426
|
+
const v = this.params[field];
|
|
427
|
+
return v === void 0 ? fallback : v;
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
function hasDomEvents() {
|
|
431
|
+
return typeof window !== "undefined" && typeof window.addEventListener === "function";
|
|
432
|
+
}
|
|
433
|
+
function onWindow(type, handler, opts) {
|
|
434
|
+
try {
|
|
435
|
+
if (hasDomEvents()) window.addEventListener(type, handler, opts);
|
|
436
|
+
} catch {
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function onDocument(type, handler, opts) {
|
|
440
|
+
try {
|
|
441
|
+
if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
|
|
442
|
+
document.addEventListener(type, handler, opts);
|
|
443
|
+
}
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
}
|
|
321
447
|
var FLUSH_INTERVAL_MS = 5e3;
|
|
322
448
|
var MAX_BUFFER = 100;
|
|
323
449
|
var ANON_ID_KEY = "__se_anon_id";
|
|
@@ -329,8 +455,8 @@ var EventBuffer = class {
|
|
|
329
455
|
this.sdkKey = sdkKey;
|
|
330
456
|
if (typeof window !== "undefined") {
|
|
331
457
|
this.timer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
|
|
332
|
-
|
|
333
|
-
|
|
458
|
+
onWindow("beforeunload", () => this.flush());
|
|
459
|
+
onDocument("visibilitychange", () => {
|
|
334
460
|
if (document.visibilityState === "hidden") this.flush(true);
|
|
335
461
|
});
|
|
336
462
|
try {
|
|
@@ -875,6 +1001,10 @@ var Engine = class _Engine {
|
|
|
875
1001
|
env;
|
|
876
1002
|
evalResult = null;
|
|
877
1003
|
anonId;
|
|
1004
|
+
// Resolves once a caller-supplied anonymousStore has been consulted (persisted
|
|
1005
|
+
// id adopted, or the minted id written back). identify() awaits it so the
|
|
1006
|
+
// stable id is settled before the first /sdk/evaluate. null when no store.
|
|
1007
|
+
anonReady = null;
|
|
878
1008
|
userId = "";
|
|
879
1009
|
buffer;
|
|
880
1010
|
telemetry;
|
|
@@ -900,10 +1030,16 @@ var Engine = class _Engine {
|
|
|
900
1030
|
this.notify();
|
|
901
1031
|
};
|
|
902
1032
|
constructor(opts) {
|
|
1033
|
+
setLogLevel(opts.logLevel);
|
|
903
1034
|
this.sdkKey = opts.sdkKey;
|
|
904
|
-
this.baseUrl = (opts.baseUrl ?? "https://
|
|
1035
|
+
this.baseUrl = (opts.baseUrl ?? "https://api.shipeasy.ai").replace(/\/$/, "");
|
|
905
1036
|
this.env = opts.env ?? "prod";
|
|
906
1037
|
this.testMode = opts.testMode === true;
|
|
1038
|
+
setInternalReportContext({
|
|
1039
|
+
side: "client",
|
|
1040
|
+
sdkVersion: version,
|
|
1041
|
+
enabled: !this.testMode && opts.disableInternalErrorReporting !== true
|
|
1042
|
+
});
|
|
907
1043
|
this.autoGuardrails = opts.autoGuardrails !== false;
|
|
908
1044
|
this.autoCollectAlways = opts.autoCollectAlways === true;
|
|
909
1045
|
this.disableAutoExposure = opts.disableAutoExposure === true;
|
|
@@ -916,6 +1052,9 @@ var Engine = class _Engine {
|
|
|
916
1052
|
engagement: g.engagement ?? this.autoGuardrails
|
|
917
1053
|
};
|
|
918
1054
|
this.anonId = getOrCreateAnonId();
|
|
1055
|
+
if (opts.anonymousStore && !this.testMode) {
|
|
1056
|
+
this.anonReady = this.hydrateAnonFromStore(opts.anonymousStore);
|
|
1057
|
+
}
|
|
919
1058
|
this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
|
|
920
1059
|
this.telemetry = new Telemetry({
|
|
921
1060
|
endpoint: opts.telemetryUrl ?? DEFAULT_TELEMETRY_URL,
|
|
@@ -931,6 +1070,23 @@ var Engine = class _Engine {
|
|
|
931
1070
|
void this.buffer.flushPendingAlias();
|
|
932
1071
|
}
|
|
933
1072
|
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Consult the caller-supplied {@link AnonymousStore}: adopt a persisted id so
|
|
1075
|
+
* the visitor buckets identically across app launches, or persist the id just
|
|
1076
|
+
* minted by getOrCreateAnonId() on a fresh device. Best-effort — any store
|
|
1077
|
+
* failure leaves the in-memory id in place.
|
|
1078
|
+
*/
|
|
1079
|
+
async hydrateAnonFromStore(store) {
|
|
1080
|
+
try {
|
|
1081
|
+
const stored = await store.get(ANON_ID_KEY);
|
|
1082
|
+
if (typeof stored === "string" && stored) {
|
|
1083
|
+
this.anonId = stored;
|
|
1084
|
+
} else {
|
|
1085
|
+
await store.set(ANON_ID_KEY, this.anonId);
|
|
1086
|
+
}
|
|
1087
|
+
} catch {
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
934
1090
|
/**
|
|
935
1091
|
* Build a no-network, immediately-usable browser client for tests
|
|
936
1092
|
* (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
|
|
@@ -958,6 +1114,10 @@ var Engine = class _Engine {
|
|
|
958
1114
|
this.notify();
|
|
959
1115
|
return;
|
|
960
1116
|
}
|
|
1117
|
+
if (this.anonReady) {
|
|
1118
|
+
await this.anonReady;
|
|
1119
|
+
this.anonReady = null;
|
|
1120
|
+
}
|
|
961
1121
|
const seq = ++this.identifySeq;
|
|
962
1122
|
const prevUserId = this.userId;
|
|
963
1123
|
if (user.user_id !== void 0) this.userId = user.user_id;
|
|
@@ -1034,7 +1194,7 @@ var Engine = class _Engine {
|
|
|
1034
1194
|
try {
|
|
1035
1195
|
l();
|
|
1036
1196
|
} catch (err) {
|
|
1037
|
-
|
|
1197
|
+
logger.warn("[shipeasy] subscriber threw:", String(err));
|
|
1038
1198
|
}
|
|
1039
1199
|
}
|
|
1040
1200
|
}
|
|
@@ -1111,54 +1271,41 @@ var Engine = class _Engine {
|
|
|
1111
1271
|
try {
|
|
1112
1272
|
return opts.decode(raw);
|
|
1113
1273
|
} catch (err) {
|
|
1114
|
-
|
|
1274
|
+
logger.warn(`[shipeasy] getConfig('${name}') decode failed:`, String(err));
|
|
1115
1275
|
return void 0;
|
|
1116
1276
|
}
|
|
1117
1277
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1278
|
+
/**
|
|
1279
|
+
* Assign the visitor within a universe: `universe("checkout").assign()`. A
|
|
1280
|
+
* universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
|
|
1281
|
+
* the returned {@link Assignment} exposes `.group` / `.get(field, fallback)`
|
|
1282
|
+
* and auto-logs one exposure when enrolled (unless `disableAutoExposure` or
|
|
1283
|
+
* `assign({ logExposure: false })`). An un-enrolled visitor still resolves
|
|
1284
|
+
* `get()` to the universe defaults. This is the sole experiment read path —
|
|
1285
|
+
* there is no `getExperiment` (ask a universe, not an experiment).
|
|
1286
|
+
*/
|
|
1287
|
+
universe(name) {
|
|
1288
|
+
return { assign: (opts) => this.assignUniverse(name, opts) };
|
|
1289
|
+
}
|
|
1290
|
+
assignUniverse(name, opts) {
|
|
1121
1291
|
this.telemetry.emit("experiment", name);
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
params
|
|
1126
|
-
};
|
|
1127
|
-
const pov = this.experimentOverrides.get(name);
|
|
1128
|
-
if (pov) {
|
|
1129
|
-
const variantParams = variants?.[pov.group];
|
|
1130
|
-
const params = { ...defaultParams, ...pov.params, ...variantParams ?? {} };
|
|
1131
|
-
return { inExperiment: true, group: pov.group, params };
|
|
1292
|
+
const defaults = this.evalResult?.universes?.[name]?.defaults ?? {};
|
|
1293
|
+
for (const [expName, pov] of this.experimentOverrides) {
|
|
1294
|
+
if (this.evalResult?.experiments[expName]?.universe !== name) continue;
|
|
1295
|
+
return new AssignmentImpl(expName, pov.group, { ...defaults, ...pov.params });
|
|
1132
1296
|
}
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
return { inExperiment: true, group: ov, params };
|
|
1297
|
+
for (const [expName, entry] of Object.entries(this.evalResult?.experiments ?? {})) {
|
|
1298
|
+
if (entry.universe !== name) continue;
|
|
1299
|
+
const ov = readExpOverride(expName);
|
|
1300
|
+
if (ov !== null) return new AssignmentImpl(expName, ov, { ...defaults, ...entry.params });
|
|
1138
1301
|
}
|
|
1139
|
-
const entry
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
try {
|
|
1145
|
-
return { inExperiment: true, group: entry.group, params: decode(entry.params) };
|
|
1146
|
-
} catch (err) {
|
|
1147
|
-
console.warn(`[shipeasy] getExperiment('${name}') decode failed:`, String(err));
|
|
1148
|
-
return notIn;
|
|
1302
|
+
for (const [expName, entry] of Object.entries(this.evalResult?.experiments ?? {})) {
|
|
1303
|
+
if (entry.universe !== name || !entry.inExperiment) continue;
|
|
1304
|
+
const shouldLog = opts?.logExposure ?? !this.disableAutoExposure;
|
|
1305
|
+
if (shouldLog) this.buffer.pushExposure(expName, entry.group, this.userId, this.anonId);
|
|
1306
|
+
return new AssignmentImpl(expName, entry.group, entry.params);
|
|
1149
1307
|
}
|
|
1150
|
-
|
|
1151
|
-
/**
|
|
1152
|
-
* Manually log an exposure for an enrolled experiment (Statsig's
|
|
1153
|
-
* `manuallyLogExposure`). Reads the cached eval result; if the visitor is in
|
|
1154
|
-
* the experiment, pushes the session-deduped exposure. Pair this with the
|
|
1155
|
-
* render of the treatment when reading with `{ logExposure: false }` (or
|
|
1156
|
-
* `disableAutoExposure: true`). No-op if the visitor isn't enrolled.
|
|
1157
|
-
*/
|
|
1158
|
-
logExposure(name) {
|
|
1159
|
-
const entry = this.evalResult?.experiments[name];
|
|
1160
|
-
if (!entry || !entry.inExperiment) return;
|
|
1161
|
-
this.buffer.pushExposure(name, entry.group, this.userId, this.anonId);
|
|
1308
|
+
return new AssignmentImpl(null, null, defaults);
|
|
1162
1309
|
}
|
|
1163
1310
|
/**
|
|
1164
1311
|
* Subscribe to state changes — fires after identify() completes and on
|
|
@@ -1167,7 +1314,7 @@ var Engine = class _Engine {
|
|
|
1167
1314
|
*/
|
|
1168
1315
|
subscribe(listener) {
|
|
1169
1316
|
this.listeners.add(listener);
|
|
1170
|
-
if (!this.overrideListenerInstalled &&
|
|
1317
|
+
if (!this.overrideListenerInstalled && hasDomEvents()) {
|
|
1171
1318
|
this.overrideListenerInstalled = true;
|
|
1172
1319
|
window.addEventListener("se:override:change", this.onOverrideChange);
|
|
1173
1320
|
}
|
|
@@ -1183,9 +1330,11 @@ var Engine = class _Engine {
|
|
|
1183
1330
|
if (typeof window === "undefined") return null;
|
|
1184
1331
|
const bridge = {
|
|
1185
1332
|
getFlag: (n) => this.getFlag(n),
|
|
1333
|
+
// Devtools reads assignments by experiment name straight off the eval
|
|
1334
|
+
// result (the public read path is universe-first; this is display-only).
|
|
1186
1335
|
getExperiment: (n) => {
|
|
1187
|
-
const
|
|
1188
|
-
return { inExperiment:
|
|
1336
|
+
const entry = this.evalResult?.experiments[n];
|
|
1337
|
+
return { inExperiment: entry?.inExperiment ?? false, group: entry?.group ?? "control" };
|
|
1189
1338
|
},
|
|
1190
1339
|
getConfig: (n) => this.getConfig(n)
|
|
1191
1340
|
};
|
|
@@ -1308,7 +1457,7 @@ function loadDevtools(opts = {}) {
|
|
|
1308
1457
|
}
|
|
1309
1458
|
}
|
|
1310
1459
|
function attachDevtools(client, opts = {}) {
|
|
1311
|
-
if (
|
|
1460
|
+
if (!hasDomEvents()) return () => {
|
|
1312
1461
|
};
|
|
1313
1462
|
const hotkey = opts.hotkey ?? "Shift+Alt+S";
|
|
1314
1463
|
const parts = hotkey.split("+");
|
|
@@ -1347,19 +1496,22 @@ function shipeasy(opts) {
|
|
|
1347
1496
|
const client = configureShipeasy({
|
|
1348
1497
|
sdkKey: opts.clientKey,
|
|
1349
1498
|
baseUrl,
|
|
1499
|
+
logLevel: opts.logLevel,
|
|
1350
1500
|
autoGuardrails: blanket,
|
|
1351
1501
|
autoGuardrailGroups: groups,
|
|
1352
1502
|
autoCollectAlways: acObj?.always === true,
|
|
1353
1503
|
disableTelemetry: opts.disableTelemetry,
|
|
1504
|
+
disableInternalErrorReporting: opts.disableInternalErrorReporting,
|
|
1354
1505
|
disableAutoExposure: opts.disableAutoExposure,
|
|
1355
1506
|
privateAttributes: opts.privateAttributes,
|
|
1356
|
-
stickyBucketing: opts.stickyBucketing
|
|
1507
|
+
stickyBucketing: opts.stickyBucketing,
|
|
1508
|
+
anonymousStore: opts.anonymousStore
|
|
1357
1509
|
});
|
|
1358
1510
|
injectI18nLoader(opts.clientKey, baseUrl, opts.i18nProfile);
|
|
1359
1511
|
flags.notifyMounted();
|
|
1360
1512
|
if (opts.autoIdentify !== false) {
|
|
1361
1513
|
void client.identify({}).catch((err) => {
|
|
1362
|
-
|
|
1514
|
+
logger.warn("[shipeasy] auto-identify failed:", String(err));
|
|
1363
1515
|
});
|
|
1364
1516
|
}
|
|
1365
1517
|
return attachDevtools(client, { adminUrl: opts.adminUrl });
|
|
@@ -1435,7 +1587,7 @@ var _mountedAndReady = false;
|
|
|
1435
1587
|
var _standaloneListeners = /* @__PURE__ */ new Set();
|
|
1436
1588
|
var _standaloneOverrideWired = false;
|
|
1437
1589
|
function wireStandaloneOverride() {
|
|
1438
|
-
if (_standaloneOverrideWired ||
|
|
1590
|
+
if (_standaloneOverrideWired || !hasDomEvents()) return;
|
|
1439
1591
|
_standaloneOverrideWired = true;
|
|
1440
1592
|
window.addEventListener("se:override:change", () => {
|
|
1441
1593
|
for (const cb of _standaloneListeners) cb();
|
|
@@ -1447,7 +1599,7 @@ var flags = {
|
|
|
1447
1599
|
},
|
|
1448
1600
|
identify(user) {
|
|
1449
1601
|
if (!_client) {
|
|
1450
|
-
|
|
1602
|
+
logger.warn("[shipeasy] flags.identify called before configureShipeasy()");
|
|
1451
1603
|
return Promise.resolve();
|
|
1452
1604
|
}
|
|
1453
1605
|
return _client.identify(user);
|
|
@@ -1460,57 +1612,66 @@ var flags = {
|
|
|
1460
1612
|
* force-static pages where SSR has no flag data.
|
|
1461
1613
|
*/
|
|
1462
1614
|
get(name, defaultValue = false) {
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1615
|
+
return safeRun("flags.get", defaultValue, () => {
|
|
1616
|
+
const bs = getBootstrap();
|
|
1617
|
+
if (bs !== null && name in bs.flags) return bs.flags[name];
|
|
1618
|
+
if (!_mountedAndReady) return defaultValue;
|
|
1619
|
+
if (_client) return _client.getFlag(name, defaultValue);
|
|
1620
|
+
return readGateOverride(name) ?? defaultValue;
|
|
1621
|
+
});
|
|
1468
1622
|
},
|
|
1469
1623
|
/** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
|
|
1470
1624
|
getDetail(name) {
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1625
|
+
return safeRun("flags.getDetail", { value: false, reason: "CLIENT_NOT_READY" }, () => {
|
|
1626
|
+
if (_client) return _client.getFlagDetail(name);
|
|
1627
|
+
const ov = readGateOverride(name);
|
|
1628
|
+
if (ov !== null) return { value: ov, reason: "OVERRIDE" };
|
|
1629
|
+
return { value: false, reason: "CLIENT_NOT_READY" };
|
|
1630
|
+
});
|
|
1475
1631
|
},
|
|
1476
1632
|
getConfig(name, decode) {
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1633
|
+
return safeRun("flags.getConfig", void 0, () => {
|
|
1634
|
+
const bs = getBootstrap();
|
|
1635
|
+
if (bs !== null && name in bs.configs) {
|
|
1636
|
+
const raw = bs.configs[name];
|
|
1637
|
+
if (!decode) return raw;
|
|
1638
|
+
try {
|
|
1639
|
+
return decode(raw);
|
|
1640
|
+
} catch {
|
|
1641
|
+
return void 0;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
if (!_mountedAndReady) return void 0;
|
|
1645
|
+
if (_client) return _client.getConfig(name, decode);
|
|
1646
|
+
const ov = readConfigOverride(name);
|
|
1647
|
+
if (ov === void 0) return void 0;
|
|
1648
|
+
if (!decode) return ov;
|
|
1481
1649
|
try {
|
|
1482
|
-
return decode(
|
|
1650
|
+
return decode(ov);
|
|
1483
1651
|
} catch {
|
|
1484
1652
|
return void 0;
|
|
1485
1653
|
}
|
|
1486
|
-
}
|
|
1487
|
-
if (!_mountedAndReady) return void 0;
|
|
1488
|
-
if (_client) return _client.getConfig(name, decode);
|
|
1489
|
-
const ov = readConfigOverride(name);
|
|
1490
|
-
if (ov === void 0) return void 0;
|
|
1491
|
-
if (!decode) return ov;
|
|
1492
|
-
try {
|
|
1493
|
-
return decode(ov);
|
|
1494
|
-
} catch {
|
|
1495
|
-
return void 0;
|
|
1496
|
-
}
|
|
1654
|
+
});
|
|
1497
1655
|
},
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1656
|
+
/**
|
|
1657
|
+
* Assign the visitor within a universe: `flags.universe("checkout").assign()`.
|
|
1658
|
+
* A universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
|
|
1659
|
+
* the returned {@link Assignment} exposes `.group` / `.get(field, fallback)`
|
|
1660
|
+
* and auto-logs one exposure when enrolled. Before configure() (or on error)
|
|
1661
|
+
* returns a safe not-enrolled handle. Replaces the removed `getExperiment` —
|
|
1662
|
+
* read experiments by universe, never by name.
|
|
1663
|
+
*/
|
|
1664
|
+
universe(name) {
|
|
1665
|
+
return {
|
|
1666
|
+
assign: (opts) => safeRun(
|
|
1667
|
+
"flags.universe.assign",
|
|
1668
|
+
new AssignmentImpl(null, null, {}),
|
|
1669
|
+
() => _client ? _client.universe(name).assign(opts) : new AssignmentImpl(null, null, {})
|
|
1670
|
+
)
|
|
1503
1671
|
};
|
|
1504
|
-
if (!_client) return fallback;
|
|
1505
|
-
return typeof decodeOrOpts === "function" ? _client.getExperiment(name, defaultParams, decodeOrOpts, variants) : _client.getExperiment(name, defaultParams, decodeOrOpts ?? {});
|
|
1506
|
-
},
|
|
1507
|
-
/** Manually log an exposure for an enrolled experiment. See
|
|
1508
|
-
* {@link Engine.logExposure}. No-op before configure(). */
|
|
1509
|
-
logExposure(name) {
|
|
1510
|
-
_client?.logExposure(name);
|
|
1511
1672
|
},
|
|
1512
1673
|
track(eventName, props) {
|
|
1513
|
-
_client?.track(eventName, props);
|
|
1674
|
+
safeRun("flags.track", void 0, () => _client?.track(eventName, props));
|
|
1514
1675
|
},
|
|
1515
1676
|
/**
|
|
1516
1677
|
* Read a killswitch. Without `switchKey`, returns true when the killswitch is
|
|
@@ -1522,15 +1683,17 @@ var flags = {
|
|
|
1522
1683
|
* available synchronously on first render.
|
|
1523
1684
|
*/
|
|
1524
1685
|
ks(name, switchKey) {
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1686
|
+
return safeRun("flags.ks", false, () => {
|
|
1687
|
+
const bs = getBootstrap();
|
|
1688
|
+
if (bs !== null && bs.killswitches && name in bs.killswitches) {
|
|
1689
|
+
const ks = bs.killswitches[name];
|
|
1690
|
+
if (typeof ks === "boolean") return switchKey === void 0 ? ks : false;
|
|
1691
|
+
if (switchKey === void 0) return false;
|
|
1692
|
+
return ks[switchKey] === true;
|
|
1693
|
+
}
|
|
1694
|
+
if (!_mountedAndReady) return false;
|
|
1695
|
+
return _client?.getKillswitch(name, switchKey) ?? false;
|
|
1696
|
+
});
|
|
1534
1697
|
},
|
|
1535
1698
|
flush() {
|
|
1536
1699
|
return _client?.flush() ?? Promise.resolve();
|
|
@@ -1587,7 +1750,7 @@ var Client = class {
|
|
|
1587
1750
|
this.engine = engine;
|
|
1588
1751
|
this.attributes = _attributes(user);
|
|
1589
1752
|
this._identify = this.engine.identify(this.attributes).catch((err) => {
|
|
1590
|
-
|
|
1753
|
+
logger.warn("[shipeasy] Client identify failed:", String(err));
|
|
1591
1754
|
});
|
|
1592
1755
|
}
|
|
1593
1756
|
/** Resolves once the engine's identify() for this user has completed. */
|
|
@@ -1595,20 +1758,40 @@ var Client = class {
|
|
|
1595
1758
|
return this._identify;
|
|
1596
1759
|
}
|
|
1597
1760
|
getFlag(name, defaultValue = false) {
|
|
1598
|
-
return this.engine.getFlag(name, defaultValue);
|
|
1761
|
+
return safeRun("Client.getFlag", defaultValue, () => this.engine.getFlag(name, defaultValue));
|
|
1599
1762
|
}
|
|
1600
1763
|
getFlagDetail(name) {
|
|
1601
|
-
return
|
|
1764
|
+
return safeRun(
|
|
1765
|
+
"Client.getFlagDetail",
|
|
1766
|
+
{ value: false, reason: "CLIENT_NOT_READY" },
|
|
1767
|
+
() => this.engine.getFlagDetail(name)
|
|
1768
|
+
);
|
|
1602
1769
|
}
|
|
1603
1770
|
getConfig(name, decodeOrOpts) {
|
|
1604
|
-
return
|
|
1771
|
+
return safeRun(
|
|
1772
|
+
"Client.getConfig",
|
|
1773
|
+
void 0,
|
|
1774
|
+
() => this.engine.getConfig(name, decodeOrOpts)
|
|
1775
|
+
);
|
|
1605
1776
|
}
|
|
1606
|
-
|
|
1607
|
-
|
|
1777
|
+
/**
|
|
1778
|
+
* Assign the visitor within a universe: `client.universe("checkout").assign()`.
|
|
1779
|
+
* A universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
|
|
1780
|
+
* returns an {@link Assignment} (`.group` / `.get(field, fallback)`) and
|
|
1781
|
+
* auto-logs one exposure when enrolled. Replaces the removed `getExperiment`.
|
|
1782
|
+
*/
|
|
1783
|
+
universe(name) {
|
|
1784
|
+
return {
|
|
1785
|
+
assign: (opts) => safeRun(
|
|
1786
|
+
"Client.universe.assign",
|
|
1787
|
+
new AssignmentImpl(null, null, {}),
|
|
1788
|
+
() => this.engine.universe(name).assign(opts)
|
|
1789
|
+
)
|
|
1790
|
+
};
|
|
1608
1791
|
}
|
|
1609
1792
|
/** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
|
|
1610
1793
|
getKillswitch(name, switchKey) {
|
|
1611
|
-
return this.engine.getKillswitch(name, switchKey);
|
|
1794
|
+
return safeRun("Client.getKillswitch", false, () => this.engine.getKillswitch(name, switchKey));
|
|
1612
1795
|
}
|
|
1613
1796
|
/**
|
|
1614
1797
|
* Record a conversion/metric event for the bound (identified) user. Delegates
|
|
@@ -1617,21 +1800,12 @@ var Client = class {
|
|
|
1617
1800
|
* test mode.
|
|
1618
1801
|
*/
|
|
1619
1802
|
track(eventName, props) {
|
|
1620
|
-
this.engine.track(eventName, props);
|
|
1621
|
-
}
|
|
1622
|
-
/**
|
|
1623
|
-
* Log an exposure for `name` at the treatment's render for the bound user.
|
|
1624
|
-
* Delegates to {@link Engine.logExposure} (no-op when the visitor isn't
|
|
1625
|
-
* enrolled). Pair with `getExperiment(name, …, { logExposure: false })` /
|
|
1626
|
-
* `disableAutoExposure` to log exposure exactly when you render.
|
|
1627
|
-
*/
|
|
1628
|
-
logExposure(name) {
|
|
1629
|
-
this.engine.logExposure(name);
|
|
1803
|
+
safeRun("Client.track", void 0, () => this.engine.track(eventName, props));
|
|
1630
1804
|
}
|
|
1631
1805
|
};
|
|
1632
1806
|
function dispatchSee(problem, consequence, extras, kind) {
|
|
1633
1807
|
if (!_client) {
|
|
1634
|
-
|
|
1808
|
+
logger.warn("[shipeasy] see() called before shipeasy({ clientKey }) \u2014 error dropped");
|
|
1635
1809
|
return;
|
|
1636
1810
|
}
|
|
1637
1811
|
_client.reportError(problem, consequence, extras, kind);
|
|
@@ -1853,6 +2027,7 @@ var i18n = {
|
|
|
1853
2027
|
whenReady() {
|
|
1854
2028
|
if (typeof window === "undefined") return Promise.resolve();
|
|
1855
2029
|
if (window.i18n?.locale) return Promise.resolve();
|
|
2030
|
+
if (!hasDomEvents()) return Promise.resolve();
|
|
1856
2031
|
return new Promise((resolve) => {
|
|
1857
2032
|
const handler = () => resolve();
|
|
1858
2033
|
window.addEventListener("se:i18n:ready", handler, { once: true });
|
|
@@ -1863,6 +2038,8 @@ var i18n = {
|
|
|
1863
2038
|
if (typeof window === "undefined") return () => {
|
|
1864
2039
|
};
|
|
1865
2040
|
if (window.i18n) return window.i18n.on("update", cb);
|
|
2041
|
+
if (!hasDomEvents()) return () => {
|
|
2042
|
+
};
|
|
1866
2043
|
let unsub = () => {
|
|
1867
2044
|
};
|
|
1868
2045
|
const handler = () => {
|
|
@@ -1883,6 +2060,7 @@ export {
|
|
|
1883
2060
|
LABEL_MARKER_RE,
|
|
1884
2061
|
LABEL_MARKER_SEP,
|
|
1885
2062
|
LABEL_MARKER_START,
|
|
2063
|
+
LOG_LEVELS,
|
|
1886
2064
|
_resetConfigureForTests,
|
|
1887
2065
|
_resetShipeasyForTests,
|
|
1888
2066
|
attachDevtools,
|