@shipeasy/sdk 6.0.0 → 6.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 +78 -351
- package/dist/client/index.d.mts +14 -0
- package/dist/client/index.d.ts +14 -0
- package/dist/client/index.js +18 -0
- package/dist/client/index.mjs +18 -0
- package/dist/openfeature-server/index.d.mts +15 -2
- package/dist/openfeature-server/index.d.ts +15 -2
- package/dist/openfeature-server/index.js +194 -4
- package/dist/openfeature-server/index.mjs +194 -4
- package/dist/server/index.d.mts +101 -1
- package/dist/server/index.d.ts +101 -1
- package/dist/server/index.js +101 -4
- package/dist/server/index.mjs +94 -4
- package/dist/skill-cli.js +56 -0
- package/docs/skill/SKILL.md +145 -0
- package/package.json +7 -1
package/dist/server/index.js
CHANGED
|
@@ -37,7 +37,10 @@ __export(server_exports, {
|
|
|
37
37
|
_murmur3ForTests: () => _murmur3ForTests,
|
|
38
38
|
_resetConfigureForTests: () => _resetConfigureForTests,
|
|
39
39
|
_resetShipeasyServerForTests: () => _resetShipeasyServerForTests,
|
|
40
|
+
clearOverrides: () => clearOverrides,
|
|
40
41
|
configure: () => configure,
|
|
42
|
+
configureForOffline: () => configureForOffline,
|
|
43
|
+
configureForTesting: () => configureForTesting,
|
|
41
44
|
configureShipeasyServer: () => configureShipeasyServer,
|
|
42
45
|
createInMemoryStickyStore: () => createInMemoryStickyStore,
|
|
43
46
|
fetchLabelsForSSR: () => fetchLabelsForSSR,
|
|
@@ -47,6 +50,10 @@ __export(server_exports, {
|
|
|
47
50
|
getShipeasyServerClient: () => getShipeasyServerClient,
|
|
48
51
|
i18n: () => i18n,
|
|
49
52
|
isExpected: () => isExpected,
|
|
53
|
+
onChange: () => onChange,
|
|
54
|
+
overrideConfig: () => overrideConfig,
|
|
55
|
+
overrideExperiment: () => overrideExperiment,
|
|
56
|
+
overrideFlag: () => overrideFlag,
|
|
50
57
|
see: () => see,
|
|
51
58
|
seeContext: () => seeContext,
|
|
52
59
|
shipeasy: () => shipeasy,
|
|
@@ -981,7 +988,11 @@ var Engine = class _Engine {
|
|
|
981
988
|
const ks = this.flagsBlob?.killswitches?.[name];
|
|
982
989
|
if (!ks) return false;
|
|
983
990
|
if (switchKey === void 0) return isEnabled(ks.killed);
|
|
984
|
-
|
|
991
|
+
const switches = ks.switches ?? {};
|
|
992
|
+
if (Object.prototype.hasOwnProperty.call(switches, switchKey)) {
|
|
993
|
+
return isEnabled(switches[switchKey]);
|
|
994
|
+
}
|
|
995
|
+
return isEnabled(ks.killed);
|
|
985
996
|
}
|
|
986
997
|
};
|
|
987
998
|
var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
|
|
@@ -1293,16 +1304,74 @@ var flags = {
|
|
|
1293
1304
|
var _identityAttributes = (user) => user && typeof user === "object" ? user : {};
|
|
1294
1305
|
var _attributes = _identityAttributes;
|
|
1295
1306
|
function configure(opts) {
|
|
1296
|
-
const { attributes, ...engineOpts } = opts;
|
|
1307
|
+
const { attributes, poll = false, init = true, ...engineOpts } = opts;
|
|
1297
1308
|
_attributes = attributes ?? _identityAttributes;
|
|
1298
1309
|
const engine = configureShipeasyServer(engineOpts);
|
|
1299
|
-
|
|
1300
|
-
|
|
1310
|
+
if (poll) {
|
|
1311
|
+
void engine.init().catch(() => {
|
|
1312
|
+
});
|
|
1313
|
+
} else if (init) {
|
|
1314
|
+
void engine.initOnce().catch(() => {
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1301
1317
|
return engine;
|
|
1302
1318
|
}
|
|
1303
1319
|
function _resetConfigureForTests() {
|
|
1304
1320
|
_attributes = _identityAttributes;
|
|
1305
1321
|
}
|
|
1322
|
+
function _installGlobalEngine(engine, attributes) {
|
|
1323
|
+
_server?.destroy();
|
|
1324
|
+
_server = engine;
|
|
1325
|
+
_attributes = attributes ?? _identityAttributes;
|
|
1326
|
+
return engine;
|
|
1327
|
+
}
|
|
1328
|
+
function _applyOverrides(engine, flags2, configs, experiments) {
|
|
1329
|
+
for (const [name, value] of Object.entries(flags2 ?? {})) engine.overrideFlag(name, value);
|
|
1330
|
+
for (const [name, value] of Object.entries(configs ?? {})) engine.overrideConfig(name, value);
|
|
1331
|
+
for (const [name, [group, params]] of Object.entries(experiments ?? {}))
|
|
1332
|
+
engine.overrideExperiment(name, group, params);
|
|
1333
|
+
}
|
|
1334
|
+
function configureForTesting(opts = {}) {
|
|
1335
|
+
const engine = Engine.forTesting();
|
|
1336
|
+
_applyOverrides(engine, opts.flags, opts.configs, opts.experiments);
|
|
1337
|
+
return _installGlobalEngine(engine, opts.attributes);
|
|
1338
|
+
}
|
|
1339
|
+
function configureForOffline(opts) {
|
|
1340
|
+
let engine;
|
|
1341
|
+
if (opts.path !== void 0) {
|
|
1342
|
+
engine = Engine.fromFile(opts.path);
|
|
1343
|
+
} else if (opts.snapshot !== void 0) {
|
|
1344
|
+
engine = Engine.fromSnapshot(opts.snapshot);
|
|
1345
|
+
} else {
|
|
1346
|
+
throw new Error("[shipeasy] configureForOffline requires either { snapshot } or { path }");
|
|
1347
|
+
}
|
|
1348
|
+
_applyOverrides(engine, opts.flags, opts.configs, opts.experiments);
|
|
1349
|
+
return _installGlobalEngine(engine, opts.attributes);
|
|
1350
|
+
}
|
|
1351
|
+
function _requireGlobal(fn) {
|
|
1352
|
+
const engine = getShipeasyServerClient();
|
|
1353
|
+
if (!engine) {
|
|
1354
|
+
throw new Error(
|
|
1355
|
+
`[shipeasy] ${fn}(...) called before configure({ apiKey }) (or a configureFor* sibling).`
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
return engine;
|
|
1359
|
+
}
|
|
1360
|
+
function overrideFlag(name, value) {
|
|
1361
|
+
_requireGlobal("overrideFlag").overrideFlag(name, value);
|
|
1362
|
+
}
|
|
1363
|
+
function overrideConfig(name, value) {
|
|
1364
|
+
_requireGlobal("overrideConfig").overrideConfig(name, value);
|
|
1365
|
+
}
|
|
1366
|
+
function overrideExperiment(name, group, params) {
|
|
1367
|
+
_requireGlobal("overrideExperiment").overrideExperiment(name, group, params);
|
|
1368
|
+
}
|
|
1369
|
+
function clearOverrides() {
|
|
1370
|
+
_requireGlobal("clearOverrides").clearOverrides();
|
|
1371
|
+
}
|
|
1372
|
+
function onChange(listener) {
|
|
1373
|
+
return _requireGlobal("onChange").onChange(listener);
|
|
1374
|
+
}
|
|
1306
1375
|
var Client = class {
|
|
1307
1376
|
engine;
|
|
1308
1377
|
/** The resolved attribute bag this handle evaluates against. */
|
|
@@ -1333,6 +1402,27 @@ var Client = class {
|
|
|
1333
1402
|
getKillswitch(name, switchKey) {
|
|
1334
1403
|
return this.engine.getKillswitch(name, switchKey);
|
|
1335
1404
|
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Record a conversion/metric event for the bound user. Derives the unit from
|
|
1407
|
+
* the resolved attribute bag (`user_id`, else `anonymous_id`) and delegates to
|
|
1408
|
+
* {@link Engine.track} — so an experiment is end-to-end Client-only (no need to
|
|
1409
|
+
* drop down to the Engine to log a conversion). Fire-and-forget; no-op in test
|
|
1410
|
+
* mode.
|
|
1411
|
+
*/
|
|
1412
|
+
track(eventName, props) {
|
|
1413
|
+
const id = this.attributes.user_id ?? this.attributes.anonymous_id;
|
|
1414
|
+
if (id === void 0) return;
|
|
1415
|
+
this.engine.track(String(id), eventName, props);
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Emit an exposure event for `name` at this server-side decision point for the
|
|
1419
|
+
* bound user. Delegates to {@link Engine.logExposure} with the resolved
|
|
1420
|
+
* attribute bag (re-evaluates enrolment; no-op when the user isn't enrolled or
|
|
1421
|
+
* in test mode).
|
|
1422
|
+
*/
|
|
1423
|
+
logExposure(name) {
|
|
1424
|
+
this.engine.logExposure(this.attributes, name);
|
|
1425
|
+
}
|
|
1336
1426
|
};
|
|
1337
1427
|
function dispatchSee(problem, consequence, extras, kind) {
|
|
1338
1428
|
if (!_server) {
|
|
@@ -1357,7 +1447,10 @@ var see = Object.assign(
|
|
|
1357
1447
|
_murmur3ForTests,
|
|
1358
1448
|
_resetConfigureForTests,
|
|
1359
1449
|
_resetShipeasyServerForTests,
|
|
1450
|
+
clearOverrides,
|
|
1360
1451
|
configure,
|
|
1452
|
+
configureForOffline,
|
|
1453
|
+
configureForTesting,
|
|
1361
1454
|
configureShipeasyServer,
|
|
1362
1455
|
createInMemoryStickyStore,
|
|
1363
1456
|
fetchLabelsForSSR,
|
|
@@ -1367,6 +1460,10 @@ var see = Object.assign(
|
|
|
1367
1460
|
getShipeasyServerClient,
|
|
1368
1461
|
i18n,
|
|
1369
1462
|
isExpected,
|
|
1463
|
+
onChange,
|
|
1464
|
+
overrideConfig,
|
|
1465
|
+
overrideExperiment,
|
|
1466
|
+
overrideFlag,
|
|
1370
1467
|
see,
|
|
1371
1468
|
seeContext,
|
|
1372
1469
|
shipeasy,
|
package/dist/server/index.mjs
CHANGED
|
@@ -934,7 +934,11 @@ var Engine = class _Engine {
|
|
|
934
934
|
const ks = this.flagsBlob?.killswitches?.[name];
|
|
935
935
|
if (!ks) return false;
|
|
936
936
|
if (switchKey === void 0) return isEnabled(ks.killed);
|
|
937
|
-
|
|
937
|
+
const switches = ks.switches ?? {};
|
|
938
|
+
if (Object.prototype.hasOwnProperty.call(switches, switchKey)) {
|
|
939
|
+
return isEnabled(switches[switchKey]);
|
|
940
|
+
}
|
|
941
|
+
return isEnabled(ks.killed);
|
|
938
942
|
}
|
|
939
943
|
};
|
|
940
944
|
var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
|
|
@@ -1246,16 +1250,74 @@ var flags = {
|
|
|
1246
1250
|
var _identityAttributes = (user) => user && typeof user === "object" ? user : {};
|
|
1247
1251
|
var _attributes = _identityAttributes;
|
|
1248
1252
|
function configure(opts) {
|
|
1249
|
-
const { attributes, ...engineOpts } = opts;
|
|
1253
|
+
const { attributes, poll = false, init = true, ...engineOpts } = opts;
|
|
1250
1254
|
_attributes = attributes ?? _identityAttributes;
|
|
1251
1255
|
const engine = configureShipeasyServer(engineOpts);
|
|
1252
|
-
|
|
1253
|
-
|
|
1256
|
+
if (poll) {
|
|
1257
|
+
void engine.init().catch(() => {
|
|
1258
|
+
});
|
|
1259
|
+
} else if (init) {
|
|
1260
|
+
void engine.initOnce().catch(() => {
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1254
1263
|
return engine;
|
|
1255
1264
|
}
|
|
1256
1265
|
function _resetConfigureForTests() {
|
|
1257
1266
|
_attributes = _identityAttributes;
|
|
1258
1267
|
}
|
|
1268
|
+
function _installGlobalEngine(engine, attributes) {
|
|
1269
|
+
_server?.destroy();
|
|
1270
|
+
_server = engine;
|
|
1271
|
+
_attributes = attributes ?? _identityAttributes;
|
|
1272
|
+
return engine;
|
|
1273
|
+
}
|
|
1274
|
+
function _applyOverrides(engine, flags2, configs, experiments) {
|
|
1275
|
+
for (const [name, value] of Object.entries(flags2 ?? {})) engine.overrideFlag(name, value);
|
|
1276
|
+
for (const [name, value] of Object.entries(configs ?? {})) engine.overrideConfig(name, value);
|
|
1277
|
+
for (const [name, [group, params]] of Object.entries(experiments ?? {}))
|
|
1278
|
+
engine.overrideExperiment(name, group, params);
|
|
1279
|
+
}
|
|
1280
|
+
function configureForTesting(opts = {}) {
|
|
1281
|
+
const engine = Engine.forTesting();
|
|
1282
|
+
_applyOverrides(engine, opts.flags, opts.configs, opts.experiments);
|
|
1283
|
+
return _installGlobalEngine(engine, opts.attributes);
|
|
1284
|
+
}
|
|
1285
|
+
function configureForOffline(opts) {
|
|
1286
|
+
let engine;
|
|
1287
|
+
if (opts.path !== void 0) {
|
|
1288
|
+
engine = Engine.fromFile(opts.path);
|
|
1289
|
+
} else if (opts.snapshot !== void 0) {
|
|
1290
|
+
engine = Engine.fromSnapshot(opts.snapshot);
|
|
1291
|
+
} else {
|
|
1292
|
+
throw new Error("[shipeasy] configureForOffline requires either { snapshot } or { path }");
|
|
1293
|
+
}
|
|
1294
|
+
_applyOverrides(engine, opts.flags, opts.configs, opts.experiments);
|
|
1295
|
+
return _installGlobalEngine(engine, opts.attributes);
|
|
1296
|
+
}
|
|
1297
|
+
function _requireGlobal(fn) {
|
|
1298
|
+
const engine = getShipeasyServerClient();
|
|
1299
|
+
if (!engine) {
|
|
1300
|
+
throw new Error(
|
|
1301
|
+
`[shipeasy] ${fn}(...) called before configure({ apiKey }) (or a configureFor* sibling).`
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
return engine;
|
|
1305
|
+
}
|
|
1306
|
+
function overrideFlag(name, value) {
|
|
1307
|
+
_requireGlobal("overrideFlag").overrideFlag(name, value);
|
|
1308
|
+
}
|
|
1309
|
+
function overrideConfig(name, value) {
|
|
1310
|
+
_requireGlobal("overrideConfig").overrideConfig(name, value);
|
|
1311
|
+
}
|
|
1312
|
+
function overrideExperiment(name, group, params) {
|
|
1313
|
+
_requireGlobal("overrideExperiment").overrideExperiment(name, group, params);
|
|
1314
|
+
}
|
|
1315
|
+
function clearOverrides() {
|
|
1316
|
+
_requireGlobal("clearOverrides").clearOverrides();
|
|
1317
|
+
}
|
|
1318
|
+
function onChange(listener) {
|
|
1319
|
+
return _requireGlobal("onChange").onChange(listener);
|
|
1320
|
+
}
|
|
1259
1321
|
var Client = class {
|
|
1260
1322
|
engine;
|
|
1261
1323
|
/** The resolved attribute bag this handle evaluates against. */
|
|
@@ -1286,6 +1348,27 @@ var Client = class {
|
|
|
1286
1348
|
getKillswitch(name, switchKey) {
|
|
1287
1349
|
return this.engine.getKillswitch(name, switchKey);
|
|
1288
1350
|
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Record a conversion/metric event for the bound user. Derives the unit from
|
|
1353
|
+
* the resolved attribute bag (`user_id`, else `anonymous_id`) and delegates to
|
|
1354
|
+
* {@link Engine.track} — so an experiment is end-to-end Client-only (no need to
|
|
1355
|
+
* drop down to the Engine to log a conversion). Fire-and-forget; no-op in test
|
|
1356
|
+
* mode.
|
|
1357
|
+
*/
|
|
1358
|
+
track(eventName, props) {
|
|
1359
|
+
const id = this.attributes.user_id ?? this.attributes.anonymous_id;
|
|
1360
|
+
if (id === void 0) return;
|
|
1361
|
+
this.engine.track(String(id), eventName, props);
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Emit an exposure event for `name` at this server-side decision point for the
|
|
1365
|
+
* bound user. Delegates to {@link Engine.logExposure} with the resolved
|
|
1366
|
+
* attribute bag (re-evaluates enrolment; no-op when the user isn't enrolled or
|
|
1367
|
+
* in test mode).
|
|
1368
|
+
*/
|
|
1369
|
+
logExposure(name) {
|
|
1370
|
+
this.engine.logExposure(this.attributes, name);
|
|
1371
|
+
}
|
|
1289
1372
|
};
|
|
1290
1373
|
function dispatchSee(problem, consequence, extras, kind) {
|
|
1291
1374
|
if (!_server) {
|
|
@@ -1309,7 +1392,10 @@ export {
|
|
|
1309
1392
|
_murmur3ForTests,
|
|
1310
1393
|
_resetConfigureForTests,
|
|
1311
1394
|
_resetShipeasyServerForTests,
|
|
1395
|
+
clearOverrides,
|
|
1312
1396
|
configure,
|
|
1397
|
+
configureForOffline,
|
|
1398
|
+
configureForTesting,
|
|
1313
1399
|
configureShipeasyServer,
|
|
1314
1400
|
createInMemoryStickyStore,
|
|
1315
1401
|
fetchLabelsForSSR,
|
|
@@ -1319,6 +1405,10 @@ export {
|
|
|
1319
1405
|
getShipeasyServerClient,
|
|
1320
1406
|
i18n,
|
|
1321
1407
|
isExpected,
|
|
1408
|
+
onChange,
|
|
1409
|
+
overrideConfig,
|
|
1410
|
+
overrideExperiment,
|
|
1411
|
+
overrideFlag,
|
|
1322
1412
|
see,
|
|
1323
1413
|
seeContext,
|
|
1324
1414
|
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/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/pages/configuration.md> ·
|
|
46
|
+
<https://shipeasy-ai.github.io/sdk/pages/flags.md> ·
|
|
47
|
+
snippets <https://shipeasy-ai.github.io/sdk/snippets/release/flags.md> ·
|
|
48
|
+
<https://shipeasy-ai.github.io/sdk/snippets/release/configs.md> ·
|
|
49
|
+
<https://shipeasy-ai.github.io/sdk/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/pages/experiments.md> · track snippet
|
|
66
|
+
<https://shipeasy-ai.github.io/sdk/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/pages/i18n.md> · snippets
|
|
80
|
+
<https://shipeasy-ai.github.io/sdk/snippets/i18n/setup.md> ·
|
|
81
|
+
<https://shipeasy-ai.github.io/sdk/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/pages/error-reporting.md> · snippet
|
|
98
|
+
<https://shipeasy-ai.github.io/sdk/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/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/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/pages/advanced.md>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipeasy/sdk",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.0",
|
|
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",
|
|
@@ -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": {
|