@shipeasy/sdk 6.0.0 → 6.3.1
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 +78 -351
- package/dist/client/index.d.mts +34 -1
- package/dist/client/index.d.ts +34 -1
- package/dist/client/index.js +142 -50
- package/dist/client/index.mjs +141 -50
- package/dist/openfeature-server/index.d.mts +27 -2
- package/dist/openfeature-server/index.d.ts +27 -2
- package/dist/openfeature-server/index.js +227 -4
- package/dist/openfeature-server/index.mjs +227 -4
- package/dist/openfeature-web/index.d.mts +10 -0
- package/dist/openfeature-web/index.d.ts +10 -0
- package/dist/server/index.d.mts +122 -1
- package/dist/server/index.d.ts +122 -1
- package/dist/server/index.js +214 -37
- package/dist/server/index.mjs +206 -37
- package/dist/skill-cli.js +56 -0
- package/docs/skill/SKILL.md +145 -0
- package/package.json +13 -6
package/dist/server/index.mjs
CHANGED
|
@@ -67,6 +67,53 @@ function send(url) {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// src/logger.ts
|
|
71
|
+
var LOG_LEVELS = ["silent", "error", "warn", "info", "debug"];
|
|
72
|
+
var RANK = {
|
|
73
|
+
silent: 0,
|
|
74
|
+
error: 1,
|
|
75
|
+
warn: 2,
|
|
76
|
+
info: 3,
|
|
77
|
+
debug: 4
|
|
78
|
+
};
|
|
79
|
+
var currentLevel = "warn";
|
|
80
|
+
function setLogLevel(level) {
|
|
81
|
+
if (typeof level === "string" && Object.prototype.hasOwnProperty.call(RANK, level)) {
|
|
82
|
+
currentLevel = level;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function write(method, args) {
|
|
86
|
+
try {
|
|
87
|
+
const c = globalThis.console;
|
|
88
|
+
if (!c) return;
|
|
89
|
+
const fn = c[method] ?? c.log;
|
|
90
|
+
if (typeof fn === "function") fn.call(c, ...args);
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
var logger = {
|
|
95
|
+
error(...args) {
|
|
96
|
+
if (RANK[currentLevel] >= RANK.error) write("error", args);
|
|
97
|
+
},
|
|
98
|
+
warn(...args) {
|
|
99
|
+
if (RANK[currentLevel] >= RANK.warn) write("warn", args);
|
|
100
|
+
},
|
|
101
|
+
info(...args) {
|
|
102
|
+
if (RANK[currentLevel] >= RANK.info) write("info", args);
|
|
103
|
+
},
|
|
104
|
+
debug(...args) {
|
|
105
|
+
if (RANK[currentLevel] >= RANK.debug) write("debug", args);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
function safeRun(label, fallback, fn) {
|
|
109
|
+
try {
|
|
110
|
+
return fn();
|
|
111
|
+
} catch (err) {
|
|
112
|
+
logger.error(`[shipeasy] ${label} failed \u2014 returning safe default:`, String(err));
|
|
113
|
+
return fallback;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
70
117
|
// src/see/core.ts
|
|
71
118
|
var SEE_MAX_MESSAGE = 500;
|
|
72
119
|
var SEE_MAX_STACK = 8e3;
|
|
@@ -528,6 +575,7 @@ var Engine = class _Engine {
|
|
|
528
575
|
// 304). Never fired in testMode/offline (no polling happens there).
|
|
529
576
|
changeListeners = /* @__PURE__ */ new Set();
|
|
530
577
|
constructor(opts) {
|
|
578
|
+
setLogLevel(opts.logLevel);
|
|
531
579
|
this.apiKey = opts.apiKey;
|
|
532
580
|
this.baseUrl = (opts.baseUrl ?? "https://cdn.shipeasy.ai").replace(/\/$/, "");
|
|
533
581
|
this.env = opts.env ?? "prod";
|
|
@@ -651,14 +699,14 @@ var Engine = class _Engine {
|
|
|
651
699
|
try {
|
|
652
700
|
l();
|
|
653
701
|
} catch (err) {
|
|
654
|
-
|
|
702
|
+
logger.warn("[shipeasy] onChange listener threw:", String(err));
|
|
655
703
|
}
|
|
656
704
|
}
|
|
657
705
|
}
|
|
658
706
|
startPoll() {
|
|
659
707
|
this.timer = setInterval(() => {
|
|
660
708
|
this.fetchAll(true).catch(
|
|
661
|
-
(err) =>
|
|
709
|
+
(err) => logger.warn("[shipeasy] background poll failed:", String(err))
|
|
662
710
|
);
|
|
663
711
|
}, this.pollInterval * 1e3);
|
|
664
712
|
}
|
|
@@ -737,7 +785,12 @@ var Engine = class _Engine {
|
|
|
737
785
|
return "defaultValue" in opts ? opts.defaultValue : void 0;
|
|
738
786
|
}
|
|
739
787
|
if (!opts.decode) return raw;
|
|
740
|
-
|
|
788
|
+
try {
|
|
789
|
+
return opts.decode(raw);
|
|
790
|
+
} catch (err) {
|
|
791
|
+
logger.warn(`[shipeasy] getConfig('${name}') decode failed:`, String(err));
|
|
792
|
+
return "defaultValue" in opts ? opts.defaultValue : void 0;
|
|
793
|
+
}
|
|
741
794
|
}
|
|
742
795
|
getExperiment(name, user, defaultParams, decode) {
|
|
743
796
|
this.telemetry.emit("experiment", name);
|
|
@@ -752,7 +805,7 @@ var Engine = class _Engine {
|
|
|
752
805
|
try {
|
|
753
806
|
return { inExperiment: true, group: ov.group, params: decode(ov.params) };
|
|
754
807
|
} catch (err) {
|
|
755
|
-
|
|
808
|
+
logger.warn(`[shipeasy] getExperiment('${name}') override decode failed:`, String(err));
|
|
756
809
|
return notIn;
|
|
757
810
|
}
|
|
758
811
|
}
|
|
@@ -777,7 +830,7 @@ var Engine = class _Engine {
|
|
|
777
830
|
try {
|
|
778
831
|
return { inExperiment: true, group: g.name, params: decode(g.params) };
|
|
779
832
|
} catch (err) {
|
|
780
|
-
|
|
833
|
+
logger.warn(`[shipeasy] getExperiment('${name}') decode failed:`, String(err));
|
|
781
834
|
return notIn;
|
|
782
835
|
}
|
|
783
836
|
};
|
|
@@ -829,7 +882,7 @@ var Engine = class _Engine {
|
|
|
829
882
|
method: "POST",
|
|
830
883
|
headers: { "X-SDK-Key": this.apiKey, "Content-Type": "text/plain" },
|
|
831
884
|
body
|
|
832
|
-
}).catch((err) =>
|
|
885
|
+
}).catch((err) => logger.warn("[shipeasy] track failed:", String(err)));
|
|
833
886
|
}
|
|
834
887
|
/**
|
|
835
888
|
* Emit an exposure event for an experiment at the server-side decision point
|
|
@@ -860,7 +913,7 @@ var Engine = class _Engine {
|
|
|
860
913
|
method: "POST",
|
|
861
914
|
headers: { "X-SDK-Key": this.apiKey, "Content-Type": "text/plain" },
|
|
862
915
|
body
|
|
863
|
-
}).catch((err) =>
|
|
916
|
+
}).catch((err) => logger.warn("[shipeasy] logExposure failed:", String(err)));
|
|
864
917
|
}
|
|
865
918
|
/**
|
|
866
919
|
* Report a structured error into the errors primitive. Fire-and-forget —
|
|
@@ -880,7 +933,7 @@ var Engine = class _Engine {
|
|
|
880
933
|
method: "POST",
|
|
881
934
|
headers: { "X-SDK-Key": this.apiKey, "Content-Type": "text/plain" },
|
|
882
935
|
body: JSON.stringify({ events: [ev] })
|
|
883
|
-
}).catch((err) =>
|
|
936
|
+
}).catch((err) => logger.warn("[shipeasy] see() send failed:", String(err)));
|
|
884
937
|
} catch {
|
|
885
938
|
}
|
|
886
939
|
}
|
|
@@ -934,7 +987,11 @@ var Engine = class _Engine {
|
|
|
934
987
|
const ks = this.flagsBlob?.killswitches?.[name];
|
|
935
988
|
if (!ks) return false;
|
|
936
989
|
if (switchKey === void 0) return isEnabled(ks.killed);
|
|
937
|
-
|
|
990
|
+
const switches = ks.switches ?? {};
|
|
991
|
+
if (Object.prototype.hasOwnProperty.call(switches, switchKey)) {
|
|
992
|
+
return isEnabled(switches[switchKey]);
|
|
993
|
+
}
|
|
994
|
+
return isEnabled(ks.killed);
|
|
938
995
|
}
|
|
939
996
|
};
|
|
940
997
|
var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
|
|
@@ -1063,7 +1120,7 @@ function _resetShipeasyServerForTests() {
|
|
|
1063
1120
|
async function shipeasy(opts) {
|
|
1064
1121
|
const serverKey = opts.serverKey ?? "";
|
|
1065
1122
|
if (!serverKey) {
|
|
1066
|
-
|
|
1123
|
+
logger.error(
|
|
1067
1124
|
"[shipeasy] No server key \u2014 flags, experiments and SSR i18n skipped. Pass `serverKey` to shipeasy() from @shipeasy/sdk/server with your server key (SHIPEASY_SERVER_KEY). Set it as a Worker secret with `wrangler secret put SHIPEASY_SERVER_KEY` (or add it to .env for local dev). Do not pass a client key here \u2014 the server entrypoint only accepts the server key."
|
|
1068
1125
|
);
|
|
1069
1126
|
}
|
|
@@ -1071,7 +1128,8 @@ async function shipeasy(opts) {
|
|
|
1071
1128
|
flags.configure({
|
|
1072
1129
|
apiKey: serverKey,
|
|
1073
1130
|
disableTelemetry: opts.disableTelemetry,
|
|
1074
|
-
privateAttributes: opts.privateAttributes
|
|
1131
|
+
privateAttributes: opts.privateAttributes,
|
|
1132
|
+
logLevel: opts.logLevel
|
|
1075
1133
|
});
|
|
1076
1134
|
let resolvedUrlOverrides = opts.urlOverrides;
|
|
1077
1135
|
if (!resolvedUrlOverrides) {
|
|
@@ -1197,21 +1255,30 @@ var flags = {
|
|
|
1197
1255
|
_server?.destroy();
|
|
1198
1256
|
},
|
|
1199
1257
|
get(name, user, defaultValue = false) {
|
|
1200
|
-
return _server?.getFlag(name, user, defaultValue) ?? defaultValue;
|
|
1258
|
+
return safeRun("flags.get", defaultValue, () => _server?.getFlag(name, user, defaultValue) ?? defaultValue);
|
|
1201
1259
|
},
|
|
1202
1260
|
/** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
|
|
1203
1261
|
getDetail(name, user) {
|
|
1204
|
-
return
|
|
1262
|
+
return safeRun(
|
|
1263
|
+
"flags.getDetail",
|
|
1264
|
+
{ value: false, reason: "CLIENT_NOT_READY" },
|
|
1265
|
+
() => _server?.getFlagDetail(name, user) ?? { value: false, reason: "CLIENT_NOT_READY" }
|
|
1266
|
+
);
|
|
1205
1267
|
},
|
|
1206
1268
|
getConfig(name, decodeOrOpts) {
|
|
1207
|
-
return
|
|
1269
|
+
return safeRun(
|
|
1270
|
+
"flags.getConfig",
|
|
1271
|
+
void 0,
|
|
1272
|
+
() => _server?.getConfig(name, decodeOrOpts)
|
|
1273
|
+
);
|
|
1208
1274
|
},
|
|
1209
1275
|
getExperiment(name, user, defaultParams, decode) {
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1276
|
+
const notIn = { inExperiment: false, group: "control", params: defaultParams };
|
|
1277
|
+
return safeRun(
|
|
1278
|
+
"flags.getExperiment",
|
|
1279
|
+
notIn,
|
|
1280
|
+
() => _server?.getExperiment(name, user, defaultParams, decode) ?? notIn
|
|
1281
|
+
);
|
|
1215
1282
|
},
|
|
1216
1283
|
/**
|
|
1217
1284
|
* Read a killswitch. Without `switchKey`, returns true when the whole
|
|
@@ -1219,15 +1286,15 @@ var flags = {
|
|
|
1219
1286
|
* switch is on. Unknown killswitches / switches return false.
|
|
1220
1287
|
*/
|
|
1221
1288
|
ks(name, switchKey) {
|
|
1222
|
-
return _server?.getKillswitch(name, switchKey) ?? false;
|
|
1289
|
+
return safeRun("flags.ks", false, () => _server?.getKillswitch(name, switchKey) ?? false);
|
|
1223
1290
|
},
|
|
1224
1291
|
track(userId, eventName, props) {
|
|
1225
|
-
_server?.track(userId, eventName, props);
|
|
1292
|
+
safeRun("flags.track", void 0, () => _server?.track(userId, eventName, props));
|
|
1226
1293
|
},
|
|
1227
1294
|
/** Emit an exposure for an enrolled experiment at the decision point. See
|
|
1228
1295
|
* {@link Engine.logExposure}. No-op before configure(). */
|
|
1229
1296
|
logExposure(user, name) {
|
|
1230
|
-
_server?.logExposure(user, name);
|
|
1297
|
+
safeRun("flags.logExposure", void 0, () => _server?.logExposure(user, name));
|
|
1231
1298
|
},
|
|
1232
1299
|
/**
|
|
1233
1300
|
* Evaluate all flags / configs / experiments for a user against the locally
|
|
@@ -1235,27 +1302,81 @@ var flags = {
|
|
|
1235
1302
|
* overrides. Returns an empty payload when the blob hasn't been fetched yet.
|
|
1236
1303
|
*/
|
|
1237
1304
|
evaluate(user, rawUrl) {
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
configs: {},
|
|
1241
|
-
experiments: {},
|
|
1242
|
-
killswitches: {}
|
|
1243
|
-
};
|
|
1305
|
+
const empty = { flags: {}, configs: {}, experiments: {}, killswitches: {} };
|
|
1306
|
+
return safeRun("flags.evaluate", empty, () => _server?.evaluate(user, rawUrl) ?? empty);
|
|
1244
1307
|
}
|
|
1245
1308
|
};
|
|
1246
1309
|
var _identityAttributes = (user) => user && typeof user === "object" ? user : {};
|
|
1247
1310
|
var _attributes = _identityAttributes;
|
|
1248
1311
|
function configure(opts) {
|
|
1249
|
-
const { attributes, ...engineOpts } = opts;
|
|
1312
|
+
const { attributes, poll = false, init = true, ...engineOpts } = opts;
|
|
1250
1313
|
_attributes = attributes ?? _identityAttributes;
|
|
1251
1314
|
const engine = configureShipeasyServer(engineOpts);
|
|
1252
|
-
|
|
1253
|
-
|
|
1315
|
+
if (poll) {
|
|
1316
|
+
void engine.init().catch(() => {
|
|
1317
|
+
});
|
|
1318
|
+
} else if (init) {
|
|
1319
|
+
void engine.initOnce().catch(() => {
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1254
1322
|
return engine;
|
|
1255
1323
|
}
|
|
1256
1324
|
function _resetConfigureForTests() {
|
|
1257
1325
|
_attributes = _identityAttributes;
|
|
1258
1326
|
}
|
|
1327
|
+
function _installGlobalEngine(engine, attributes) {
|
|
1328
|
+
_server?.destroy();
|
|
1329
|
+
_server = engine;
|
|
1330
|
+
_attributes = attributes ?? _identityAttributes;
|
|
1331
|
+
return engine;
|
|
1332
|
+
}
|
|
1333
|
+
function _applyOverrides(engine, flags2, configs, experiments) {
|
|
1334
|
+
for (const [name, value] of Object.entries(flags2 ?? {})) engine.overrideFlag(name, value);
|
|
1335
|
+
for (const [name, value] of Object.entries(configs ?? {})) engine.overrideConfig(name, value);
|
|
1336
|
+
for (const [name, [group, params]] of Object.entries(experiments ?? {}))
|
|
1337
|
+
engine.overrideExperiment(name, group, params);
|
|
1338
|
+
}
|
|
1339
|
+
function configureForTesting(opts = {}) {
|
|
1340
|
+
const engine = Engine.forTesting();
|
|
1341
|
+
_applyOverrides(engine, opts.flags, opts.configs, opts.experiments);
|
|
1342
|
+
return _installGlobalEngine(engine, opts.attributes);
|
|
1343
|
+
}
|
|
1344
|
+
function configureForOffline(opts) {
|
|
1345
|
+
let engine;
|
|
1346
|
+
if (opts.path !== void 0) {
|
|
1347
|
+
engine = Engine.fromFile(opts.path);
|
|
1348
|
+
} else if (opts.snapshot !== void 0) {
|
|
1349
|
+
engine = Engine.fromSnapshot(opts.snapshot);
|
|
1350
|
+
} else {
|
|
1351
|
+
throw new Error("[shipeasy] configureForOffline requires either { snapshot } or { path }");
|
|
1352
|
+
}
|
|
1353
|
+
_applyOverrides(engine, opts.flags, opts.configs, opts.experiments);
|
|
1354
|
+
return _installGlobalEngine(engine, opts.attributes);
|
|
1355
|
+
}
|
|
1356
|
+
function _requireGlobal(fn) {
|
|
1357
|
+
const engine = getShipeasyServerClient();
|
|
1358
|
+
if (!engine) {
|
|
1359
|
+
throw new Error(
|
|
1360
|
+
`[shipeasy] ${fn}(...) called before configure({ apiKey }) (or a configureFor* sibling).`
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
return engine;
|
|
1364
|
+
}
|
|
1365
|
+
function overrideFlag(name, value) {
|
|
1366
|
+
_requireGlobal("overrideFlag").overrideFlag(name, value);
|
|
1367
|
+
}
|
|
1368
|
+
function overrideConfig(name, value) {
|
|
1369
|
+
_requireGlobal("overrideConfig").overrideConfig(name, value);
|
|
1370
|
+
}
|
|
1371
|
+
function overrideExperiment(name, group, params) {
|
|
1372
|
+
_requireGlobal("overrideExperiment").overrideExperiment(name, group, params);
|
|
1373
|
+
}
|
|
1374
|
+
function clearOverrides() {
|
|
1375
|
+
_requireGlobal("clearOverrides").clearOverrides();
|
|
1376
|
+
}
|
|
1377
|
+
function onChange(listener) {
|
|
1378
|
+
return _requireGlobal("onChange").onChange(listener);
|
|
1379
|
+
}
|
|
1259
1380
|
var Client = class {
|
|
1260
1381
|
engine;
|
|
1261
1382
|
/** The resolved attribute bag this handle evaluates against. */
|
|
@@ -1271,25 +1392,65 @@ var Client = class {
|
|
|
1271
1392
|
this.attributes = _attributes(user);
|
|
1272
1393
|
}
|
|
1273
1394
|
getFlag(name, defaultValue = false) {
|
|
1274
|
-
return
|
|
1395
|
+
return safeRun(
|
|
1396
|
+
"Client.getFlag",
|
|
1397
|
+
defaultValue,
|
|
1398
|
+
() => this.engine.getFlag(name, this.attributes, defaultValue)
|
|
1399
|
+
);
|
|
1275
1400
|
}
|
|
1276
1401
|
getFlagDetail(name) {
|
|
1277
|
-
return
|
|
1402
|
+
return safeRun(
|
|
1403
|
+
"Client.getFlagDetail",
|
|
1404
|
+
{ value: false, reason: "CLIENT_NOT_READY" },
|
|
1405
|
+
() => this.engine.getFlagDetail(name, this.attributes)
|
|
1406
|
+
);
|
|
1278
1407
|
}
|
|
1279
1408
|
getConfig(name, decodeOrOpts) {
|
|
1280
|
-
return
|
|
1409
|
+
return safeRun(
|
|
1410
|
+
"Client.getConfig",
|
|
1411
|
+
void 0,
|
|
1412
|
+
() => this.engine.getConfig(name, decodeOrOpts)
|
|
1413
|
+
);
|
|
1281
1414
|
}
|
|
1282
1415
|
getExperiment(name, defaultParams, decode) {
|
|
1283
|
-
|
|
1416
|
+
const notIn = { inExperiment: false, group: "control", params: defaultParams };
|
|
1417
|
+
return safeRun(
|
|
1418
|
+
"Client.getExperiment",
|
|
1419
|
+
notIn,
|
|
1420
|
+
() => this.engine.getExperiment(name, this.attributes, defaultParams, decode)
|
|
1421
|
+
);
|
|
1284
1422
|
}
|
|
1285
1423
|
/** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
|
|
1286
1424
|
getKillswitch(name, switchKey) {
|
|
1287
|
-
return this.engine.getKillswitch(name, switchKey);
|
|
1425
|
+
return safeRun("Client.getKillswitch", false, () => this.engine.getKillswitch(name, switchKey));
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Record a conversion/metric event for the bound user. Derives the unit from
|
|
1429
|
+
* the resolved attribute bag (`user_id`, else `anonymous_id`) and delegates to
|
|
1430
|
+
* {@link Engine.track} — so an experiment is end-to-end Client-only (no need to
|
|
1431
|
+
* drop down to the Engine to log a conversion). Fire-and-forget; no-op in test
|
|
1432
|
+
* mode.
|
|
1433
|
+
*/
|
|
1434
|
+
track(eventName, props) {
|
|
1435
|
+
safeRun("Client.track", void 0, () => {
|
|
1436
|
+
const id = this.attributes.user_id ?? this.attributes.anonymous_id;
|
|
1437
|
+
if (id === void 0) return;
|
|
1438
|
+
this.engine.track(String(id), eventName, props);
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Emit an exposure event for `name` at this server-side decision point for the
|
|
1443
|
+
* bound user. Delegates to {@link Engine.logExposure} with the resolved
|
|
1444
|
+
* attribute bag (re-evaluates enrolment; no-op when the user isn't enrolled or
|
|
1445
|
+
* in test mode).
|
|
1446
|
+
*/
|
|
1447
|
+
logExposure(name) {
|
|
1448
|
+
safeRun("Client.logExposure", void 0, () => this.engine.logExposure(this.attributes, name));
|
|
1288
1449
|
}
|
|
1289
1450
|
};
|
|
1290
1451
|
function dispatchSee(problem, consequence, extras, kind) {
|
|
1291
1452
|
if (!_server) {
|
|
1292
|
-
|
|
1453
|
+
logger.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
|
|
1293
1454
|
return;
|
|
1294
1455
|
}
|
|
1295
1456
|
_server.reportError(problem, consequence, extras, kind);
|
|
@@ -1306,10 +1467,14 @@ export {
|
|
|
1306
1467
|
Client,
|
|
1307
1468
|
Engine,
|
|
1308
1469
|
FLAG_REASONS,
|
|
1470
|
+
LOG_LEVELS,
|
|
1309
1471
|
_murmur3ForTests,
|
|
1310
1472
|
_resetConfigureForTests,
|
|
1311
1473
|
_resetShipeasyServerForTests,
|
|
1474
|
+
clearOverrides,
|
|
1312
1475
|
configure,
|
|
1476
|
+
configureForOffline,
|
|
1477
|
+
configureForTesting,
|
|
1313
1478
|
configureShipeasyServer,
|
|
1314
1479
|
createInMemoryStickyStore,
|
|
1315
1480
|
fetchLabelsForSSR,
|
|
@@ -1319,6 +1484,10 @@ export {
|
|
|
1319
1484
|
getShipeasyServerClient,
|
|
1320
1485
|
i18n,
|
|
1321
1486
|
isExpected,
|
|
1487
|
+
onChange,
|
|
1488
|
+
overrideConfig,
|
|
1489
|
+
overrideExperiment,
|
|
1490
|
+
overrideFlag,
|
|
1322
1491
|
see,
|
|
1323
1492
|
seeContext,
|
|
1324
1493
|
shipeasy,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/skill-cli.ts
|
|
5
|
+
var import_node_fs = require("fs");
|
|
6
|
+
var import_node_path = require("path");
|
|
7
|
+
var DEFAULT_DEST = ".claude/skills/shipeasy-typescript/SKILL.md";
|
|
8
|
+
function skillText() {
|
|
9
|
+
const candidates = [
|
|
10
|
+
(0, import_node_path.join)(__dirname, "..", "docs", "skill", "SKILL.md"),
|
|
11
|
+
(0, import_node_path.join)(__dirname, "..", "..", "docs", "skill", "SKILL.md")
|
|
12
|
+
];
|
|
13
|
+
for (const p of candidates) {
|
|
14
|
+
try {
|
|
15
|
+
if ((0, import_node_fs.existsSync)(p)) return (0, import_node_fs.readFileSync)(p, "utf8");
|
|
16
|
+
} catch {
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
throw new Error("shipeasy-skill: bundled SKILL.md not found in the package.");
|
|
20
|
+
}
|
|
21
|
+
function install(dir, force) {
|
|
22
|
+
let dest = (0, import_node_path.resolve)(dir);
|
|
23
|
+
const looksLikeDir = (0, import_node_fs.existsSync)(dest) && (0, import_node_fs.statSync)(dest).isDirectory() || !/\.[^/]+$/.test(dest);
|
|
24
|
+
if (looksLikeDir) dest = (0, import_node_path.join)(dest, "SKILL.md");
|
|
25
|
+
if ((0, import_node_fs.existsSync)(dest) && !force) {
|
|
26
|
+
process.stderr.write(`shipeasy-skill: refusing to overwrite ${dest} \u2014 pass --force
|
|
27
|
+
`);
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(dest), { recursive: true });
|
|
31
|
+
(0, import_node_fs.writeFileSync)(dest, skillText(), "utf8");
|
|
32
|
+
process.stdout.write(`shipeasy-skill: installed the Shipeasy agent skill \u2192 ${dest}
|
|
33
|
+
`);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
function main(argv) {
|
|
37
|
+
const [cmd, ...rest] = argv;
|
|
38
|
+
if (cmd === "print") {
|
|
39
|
+
process.stdout.write(skillText());
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
if (cmd === "install") {
|
|
43
|
+
let dir = DEFAULT_DEST;
|
|
44
|
+
let force = false;
|
|
45
|
+
for (let i = 0; i < rest.length; i++) {
|
|
46
|
+
if (rest[i] === "--force") force = true;
|
|
47
|
+
else if (rest[i] === "--dir") dir = rest[++i] ?? DEFAULT_DEST;
|
|
48
|
+
}
|
|
49
|
+
return install(dir, force);
|
|
50
|
+
}
|
|
51
|
+
process.stdout.write(
|
|
52
|
+
"shipeasy-skill \u2014 install the Shipeasy agent skill.\n\n shipeasy-skill install [--dir <path>] [--force]\n shipeasy-skill print\n"
|
|
53
|
+
);
|
|
54
|
+
return cmd && cmd !== "--help" && cmd !== "-h" ? 1 : 0;
|
|
55
|
+
}
|
|
56
|
+
process.exit(main(process.argv.slice(2)));
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: shipeasy-typescript
|
|
3
|
+
description: Use Shipeasy (feature flags, configs, kill switches, A/B experiments, i18n) from TypeScript / JavaScript. Covers configure() + Client(user), getFlag/getConfig/getKillswitch/getExperiment, track, testing, OpenFeature, and the see() error reporter — server (@shipeasy/sdk/server) and browser (@shipeasy/sdk/client).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Shipeasy TypeScript SDK
|
|
7
|
+
|
|
8
|
+
`@shipeasy/sdk` — one package, two entrypoints: `@shipeasy/sdk/server` (Node /
|
|
9
|
+
Cloudflare Worker / Deno, **server** key) and `@shipeasy/sdk/client` (browser,
|
|
10
|
+
public **client** key). Everything works from vanilla JS.
|
|
11
|
+
|
|
12
|
+
> The documented surface is exactly **`configure()`** (setup) and the bound
|
|
13
|
+
> **`new Client(user)`** (use), plus the package-level helpers below. For deeper
|
|
14
|
+
> docs, fetch any page/snippet from the manifest at
|
|
15
|
+
> <https://shipeasy-ai.github.io/sdk-ts/manifest.json> (raw page/snippet URLs below).
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @shipeasy/sdk
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Configure once, evaluate per user
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { configure, Client } from "@shipeasy/sdk/server"; // or "@shipeasy/sdk/client"
|
|
27
|
+
|
|
28
|
+
configure({
|
|
29
|
+
apiKey: process.env.SHIPEASY_SERVER_KEY!, // browser: clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY
|
|
30
|
+
attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan }), // optional; omit = identity
|
|
31
|
+
// poll: true // long-running server: keep flags fresh with a background poll (no engine.init() needed)
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const flags = new Client(currentUser); // browser: await flags.ready() before first read
|
|
35
|
+
|
|
36
|
+
flags.getFlag("new_checkout"); // boolean (2nd arg = default if not-ready/not-found)
|
|
37
|
+
flags.getConfig<{ max: number }>("limits", { defaultValue: { max: 50 } });
|
|
38
|
+
flags.getKillswitch("payments"); // global on/off (not user-bound)
|
|
39
|
+
flags.getFlagDetail("new_checkout"); // { value, reason }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`configure()` is first-config-wins and owns the fetch lifecycle (one-shot by
|
|
43
|
+
default; `poll: true` for a background refresh). Construct `new Client(user)` once
|
|
44
|
+
per user/request — it binds the user, so no method takes a user argument.
|
|
45
|
+
Full reference: <https://shipeasy-ai.github.io/sdk-ts/pages/configuration.md> ·
|
|
46
|
+
<https://shipeasy-ai.github.io/sdk-ts/pages/flags.md> ·
|
|
47
|
+
snippets <https://shipeasy-ai.github.io/sdk-ts/snippets/release/flags.md> ·
|
|
48
|
+
<https://shipeasy-ai.github.io/sdk-ts/snippets/release/configs.md> ·
|
|
49
|
+
<https://shipeasy-ai.github.io/sdk-ts/snippets/release/killswitches.md>
|
|
50
|
+
|
|
51
|
+
## Experiments + track (Client-only, end to end)
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const flags = new Client(currentUser); // construct once per callsite
|
|
55
|
+
const { inExperiment, group, params } = flags.getExperiment("hero_cta", {
|
|
56
|
+
primary_label: "Sign up", // default params (control / not-enrolled)
|
|
57
|
+
});
|
|
58
|
+
render(params.primary_label);
|
|
59
|
+
|
|
60
|
+
flags.logExposure("hero_cta"); // record the exposure where you present it
|
|
61
|
+
flags.track("purchase", { value: 42 }); // record a conversion for the bound user
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`ExperimentResult = { inExperiment: boolean; group: string; params: P }`. Full
|
|
65
|
+
reference: <https://shipeasy-ai.github.io/sdk-ts/pages/experiments.md> · track snippet
|
|
66
|
+
<https://shipeasy-ai.github.io/sdk-ts/snippets/metrics/track.md>
|
|
67
|
+
|
|
68
|
+
## i18n
|
|
69
|
+
|
|
70
|
+
Full i18n ships in this SDK. Wire the loader via the SSR bootstrap (no separate
|
|
71
|
+
init), then render with `i18n.t`:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { i18n } from "@shipeasy/sdk/client";
|
|
75
|
+
i18n.t("checkout.cta", "Place order");
|
|
76
|
+
i18n.t("cart.count", "{count} items", { count: cart.length });
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Full reference: <https://shipeasy-ai.github.io/sdk-ts/pages/i18n.md> · snippets
|
|
80
|
+
<https://shipeasy-ai.github.io/sdk-ts/snippets/i18n/setup.md> ·
|
|
81
|
+
<https://shipeasy-ai.github.io/sdk-ts/snippets/i18n/render.md>
|
|
82
|
+
|
|
83
|
+
## Error reporting (see)
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { see } from "@shipeasy/sdk/server"; // or /client
|
|
87
|
+
|
|
88
|
+
try { await submitOrder(order); }
|
|
89
|
+
catch (e) { see(e).causes_the("checkout").to("use cached prices").extras({ order_id: order.id }); }
|
|
90
|
+
|
|
91
|
+
see.Violation("large query").causes_the("results").to("be trimmed").extras({ rows });
|
|
92
|
+
see.ControlFlowException(e).because("because it wasn't an encoded Foo"); // expected — reports nothing
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Fire-and-forget on the next microtask (no `.send()`). Don't catch what you can't
|
|
96
|
+
name a consequence for. You may `see()` then re-throw (links as `caused_by`). Full
|
|
97
|
+
reference: <https://shipeasy-ai.github.io/sdk-ts/pages/error-reporting.md> · snippet
|
|
98
|
+
<https://shipeasy-ai.github.io/sdk-ts/snippets/ops/see.md>
|
|
99
|
+
|
|
100
|
+
## Testing — no network
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { configureForTesting, configureForOffline, Client, overrideFlag, clearOverrides } from "@shipeasy/sdk/server";
|
|
104
|
+
|
|
105
|
+
// Seed values up front; reads go through the ordinary new Client(user). Replaces
|
|
106
|
+
// prior config, so each test can reconfigure freely.
|
|
107
|
+
configureForTesting({
|
|
108
|
+
flags: { new_checkout: true },
|
|
109
|
+
configs: { limits: { max: 50 } },
|
|
110
|
+
experiments: { hero_cta: ["treatment", { primary_label: "Buy now" }] },
|
|
111
|
+
});
|
|
112
|
+
const flags = new Client({ user_id: "u_1" });
|
|
113
|
+
flags.getFlag("new_checkout"); // true
|
|
114
|
+
|
|
115
|
+
overrideFlag("new_checkout", false); // flip on the spot
|
|
116
|
+
clearOverrides(); // drop every override (incl. the seed)
|
|
117
|
+
|
|
118
|
+
// Offline: evaluate the REAL rules from a snapshot or JSON file, no network.
|
|
119
|
+
configureForOffline({ path: "./shipeasy-snapshot.json" });
|
|
120
|
+
// or: configureForOffline({ snapshot: { flags, experiments }, flags: { new_checkout: true } });
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Full reference: <https://shipeasy-ai.github.io/sdk-ts/pages/testing.md>
|
|
124
|
+
|
|
125
|
+
## OpenFeature
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { OpenFeature } from "@openfeature/server-sdk";
|
|
129
|
+
import { ShipeasyProvider } from "@shipeasy/sdk/openfeature-server"; // or /openfeature-web
|
|
130
|
+
|
|
131
|
+
// Assumes configure({ apiKey }) ran at startup — the no-arg provider resolves it.
|
|
132
|
+
await OpenFeature.setProviderAndWait(new ShipeasyProvider());
|
|
133
|
+
await OpenFeature.getClient().getBooleanValue("new_checkout", false, { targetingKey: "u1" });
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Full reference: <https://shipeasy-ai.github.io/sdk-ts/pages/openfeature.md>
|
|
137
|
+
|
|
138
|
+
## Advanced
|
|
139
|
+
|
|
140
|
+
`privateAttributes`, `bucketBy` (custom bucketing unit), `stickyBucketing`
|
|
141
|
+
(browser: on by default, `__se_sticky` cookie), manual exposure
|
|
142
|
+
(`getExperiment(..., { logExposure: false })` + `flags.logExposure(name)`),
|
|
143
|
+
`onChange(cb)` (requires `configure({ poll: true })`) / browser `subscribe()`.
|
|
144
|
+
Devtools overlay: `Shift+Alt+S` or `?se=1`. Full reference:
|
|
145
|
+
<https://shipeasy-ai.github.io/sdk-ts/pages/advanced.md>
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipeasy/sdk",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.1",
|
|
4
4
|
"description": "Shipeasy SDK — feature gates, runtime configs, experiments, and metrics for the Shipeasy hosted service.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://shipeasy.ai",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/shipeasy-ai/sdk.git"
|
|
9
|
+
"url": "https://github.com/shipeasy-ai/sdk-ts.git"
|
|
10
10
|
},
|
|
11
11
|
"bugs": {
|
|
12
|
-
"url": "https://github.com/shipeasy-ai/sdk/issues"
|
|
12
|
+
"url": "https://github.com/shipeasy-ai/sdk-ts/issues"
|
|
13
13
|
},
|
|
14
14
|
"main": "./dist/server/index.js",
|
|
15
15
|
"module": "./dist/server/index.mjs",
|
|
@@ -59,12 +59,17 @@
|
|
|
59
59
|
},
|
|
60
60
|
"./templates/*": "./templates/*.js"
|
|
61
61
|
},
|
|
62
|
+
"bin": {
|
|
63
|
+
"shipeasy-skill": "./dist/skill-cli.js"
|
|
64
|
+
},
|
|
62
65
|
"files": [
|
|
63
66
|
"dist/server/",
|
|
64
67
|
"dist/client/",
|
|
65
68
|
"dist/next/",
|
|
66
69
|
"dist/openfeature-server/",
|
|
67
70
|
"dist/openfeature-web/",
|
|
71
|
+
"dist/skill-cli.js",
|
|
72
|
+
"docs/skill/SKILL.md",
|
|
68
73
|
"templates/",
|
|
69
74
|
"LICENSE",
|
|
70
75
|
"README.md"
|
|
@@ -74,6 +79,7 @@
|
|
|
74
79
|
"type-check": "tsc --noEmit",
|
|
75
80
|
"test": "vitest run",
|
|
76
81
|
"test:watch": "vitest",
|
|
82
|
+
"gen:readme": "node scripts/gen-readme.mjs",
|
|
77
83
|
"publish-loader": "wrangler r2 object put shipeasy-sdk/loader-v$npm_package_version.js --file=dist/loader/loader.global.js --content-type 'application/javascript; charset=utf-8' --cache-control 'public, max-age=31536000, immutable' --remote && wrangler r2 object put shipeasy-sdk/loader.js --file=dist/loader/loader.global.js --content-type 'application/javascript; charset=utf-8' --cache-control 'public, max-age=300' --remote"
|
|
78
84
|
},
|
|
79
85
|
"dependencies": {
|
|
@@ -99,11 +105,12 @@
|
|
|
99
105
|
"@openfeature/server-sdk": "^1.22.0",
|
|
100
106
|
"@openfeature/web-sdk": "^1.9.0",
|
|
101
107
|
"@types/murmurhash-js": "^1.0.6",
|
|
102
|
-
"@types/node": "^
|
|
108
|
+
"@types/node": "^26.0.1",
|
|
103
109
|
"next": "^16.2.3",
|
|
104
110
|
"tsup": "^8.3.0",
|
|
105
|
-
"typescript": "^
|
|
106
|
-
"
|
|
111
|
+
"typescript": "^6.0.3",
|
|
112
|
+
"vite": "^6.4.3",
|
|
113
|
+
"vitest": "^4.1.9",
|
|
107
114
|
"wrangler": "^4.83.0"
|
|
108
115
|
},
|
|
109
116
|
"publishConfig": {
|