@shipeasy/sdk 5.4.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 -313
- package/dist/client/index.d.mts +102 -16
- package/dist/client/index.d.ts +102 -16
- package/dist/client/index.js +81 -8
- package/dist/client/index.mjs +77 -7
- package/dist/openfeature-server/index.d.mts +33 -20
- package/dist/openfeature-server/index.d.ts +33 -20
- package/dist/openfeature-server/index.js +194 -4
- package/dist/openfeature-server/index.mjs +194 -4
- package/dist/openfeature-web/index.d.mts +12 -12
- package/dist/openfeature-web/index.d.ts +12 -12
- package/dist/server/index.d.mts +180 -21
- package/dist/server/index.d.ts +180 -21
- package/dist/server/index.js +159 -12
- package/dist/server/index.mjs +148 -11
- package/dist/skill-cli.js +56 -0
- package/docs/skill/SKILL.md +145 -0
- package/package.json +7 -1
package/dist/server/index.mjs
CHANGED
|
@@ -499,7 +499,7 @@ function parseOverrides(rawUrl) {
|
|
|
499
499
|
}
|
|
500
500
|
return { gates, configs, experiments };
|
|
501
501
|
}
|
|
502
|
-
var
|
|
502
|
+
var Engine = class _Engine {
|
|
503
503
|
apiKey;
|
|
504
504
|
baseUrl;
|
|
505
505
|
env;
|
|
@@ -514,7 +514,7 @@ var FlagsClient = class _FlagsClient {
|
|
|
514
514
|
pollInterval = 30;
|
|
515
515
|
timer = null;
|
|
516
516
|
initialized = false;
|
|
517
|
-
// Test mode: built by `
|
|
517
|
+
// Test mode: built by `Engine.forTesting()`. When set, init()/initOnce()
|
|
518
518
|
// never fetch, track() is a no-op, and telemetry is off — the client is a
|
|
519
519
|
// fully self-contained, network-free seam for unit tests.
|
|
520
520
|
testMode;
|
|
@@ -555,13 +555,13 @@ var FlagsClient = class _FlagsClient {
|
|
|
555
555
|
* with overrideFlag/overrideConfig/overrideExperiment. No SDK key required.
|
|
556
556
|
*
|
|
557
557
|
* ```ts
|
|
558
|
-
* const client =
|
|
558
|
+
* const client = Engine.forTesting();
|
|
559
559
|
* client.overrideFlag("new_checkout", true);
|
|
560
560
|
* client.getFlag("new_checkout", { user_id: "u1" }); // true
|
|
561
561
|
* ```
|
|
562
562
|
*/
|
|
563
563
|
static forTesting(opts) {
|
|
564
|
-
return new
|
|
564
|
+
return new _Engine({ apiKey: "", ...opts, testMode: true });
|
|
565
565
|
}
|
|
566
566
|
/**
|
|
567
567
|
* Build a fully OFFLINE client from a pre-captured snapshot — no network ever.
|
|
@@ -574,7 +574,7 @@ var FlagsClient = class _FlagsClient {
|
|
|
574
574
|
* `{ flags: <GET /sdk/flags body>, experiments: <GET /sdk/experiments body> }`.
|
|
575
575
|
*/
|
|
576
576
|
static fromSnapshot(snapshot) {
|
|
577
|
-
const client = new
|
|
577
|
+
const client = new _Engine({ apiKey: "", testMode: true });
|
|
578
578
|
client.flagsBlob = snapshot.flags;
|
|
579
579
|
client.expsBlob = snapshot.experiments;
|
|
580
580
|
client.initialized = true;
|
|
@@ -584,13 +584,13 @@ var FlagsClient = class _FlagsClient {
|
|
|
584
584
|
* Build a fully OFFLINE client from a snapshot JSON file on disk (Node only —
|
|
585
585
|
* not available in the browser entrypoint). The file must contain
|
|
586
586
|
* `{ "flags": <GET /sdk/flags body>, "experiments": <GET /sdk/experiments body> }`.
|
|
587
|
-
* See {@link
|
|
587
|
+
* See {@link Engine.fromSnapshot}.
|
|
588
588
|
*/
|
|
589
589
|
static fromFile(path) {
|
|
590
590
|
const fs = __require("fs");
|
|
591
591
|
const raw = fs.readFileSync(path, "utf8");
|
|
592
592
|
const snapshot = JSON.parse(raw);
|
|
593
|
-
return
|
|
593
|
+
return _Engine.fromSnapshot(snapshot);
|
|
594
594
|
}
|
|
595
595
|
async init() {
|
|
596
596
|
if (this.testMode) {
|
|
@@ -934,7 +934,11 @@ var FlagsClient = class _FlagsClient {
|
|
|
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");
|
|
@@ -1050,7 +1054,7 @@ async function fetchLabelsForSSR(opts) {
|
|
|
1050
1054
|
var _server = null;
|
|
1051
1055
|
function configureShipeasyServer(opts) {
|
|
1052
1056
|
if (_server) return _server;
|
|
1053
|
-
_server = new
|
|
1057
|
+
_server = new Engine(opts);
|
|
1054
1058
|
return _server;
|
|
1055
1059
|
}
|
|
1056
1060
|
function getShipeasyServerClient() {
|
|
@@ -1225,7 +1229,7 @@ var flags = {
|
|
|
1225
1229
|
_server?.track(userId, eventName, props);
|
|
1226
1230
|
},
|
|
1227
1231
|
/** Emit an exposure for an enrolled experiment at the decision point. See
|
|
1228
|
-
* {@link
|
|
1232
|
+
* {@link Engine.logExposure}. No-op before configure(). */
|
|
1229
1233
|
logExposure(user, name) {
|
|
1230
1234
|
_server?.logExposure(user, name);
|
|
1231
1235
|
},
|
|
@@ -1243,6 +1247,129 @@ var flags = {
|
|
|
1243
1247
|
};
|
|
1244
1248
|
}
|
|
1245
1249
|
};
|
|
1250
|
+
var _identityAttributes = (user) => user && typeof user === "object" ? user : {};
|
|
1251
|
+
var _attributes = _identityAttributes;
|
|
1252
|
+
function configure(opts) {
|
|
1253
|
+
const { attributes, poll = false, init = true, ...engineOpts } = opts;
|
|
1254
|
+
_attributes = attributes ?? _identityAttributes;
|
|
1255
|
+
const engine = configureShipeasyServer(engineOpts);
|
|
1256
|
+
if (poll) {
|
|
1257
|
+
void engine.init().catch(() => {
|
|
1258
|
+
});
|
|
1259
|
+
} else if (init) {
|
|
1260
|
+
void engine.initOnce().catch(() => {
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
return engine;
|
|
1264
|
+
}
|
|
1265
|
+
function _resetConfigureForTests() {
|
|
1266
|
+
_attributes = _identityAttributes;
|
|
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
|
+
}
|
|
1321
|
+
var Client = class {
|
|
1322
|
+
engine;
|
|
1323
|
+
/** The resolved attribute bag this handle evaluates against. */
|
|
1324
|
+
attributes;
|
|
1325
|
+
constructor(user) {
|
|
1326
|
+
const engine = getShipeasyServerClient();
|
|
1327
|
+
if (!engine) {
|
|
1328
|
+
throw new Error(
|
|
1329
|
+
"[shipeasy] new Client(user) called before configure({ apiKey }). Call configure() once at app boot from @shipeasy/sdk/server."
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
this.engine = engine;
|
|
1333
|
+
this.attributes = _attributes(user);
|
|
1334
|
+
}
|
|
1335
|
+
getFlag(name, defaultValue = false) {
|
|
1336
|
+
return this.engine.getFlag(name, this.attributes, defaultValue);
|
|
1337
|
+
}
|
|
1338
|
+
getFlagDetail(name) {
|
|
1339
|
+
return this.engine.getFlagDetail(name, this.attributes);
|
|
1340
|
+
}
|
|
1341
|
+
getConfig(name, decodeOrOpts) {
|
|
1342
|
+
return this.engine.getConfig(name, decodeOrOpts);
|
|
1343
|
+
}
|
|
1344
|
+
getExperiment(name, defaultParams, decode) {
|
|
1345
|
+
return this.engine.getExperiment(name, this.attributes, defaultParams, decode);
|
|
1346
|
+
}
|
|
1347
|
+
/** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
|
|
1348
|
+
getKillswitch(name, switchKey) {
|
|
1349
|
+
return this.engine.getKillswitch(name, switchKey);
|
|
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
|
+
}
|
|
1372
|
+
};
|
|
1246
1373
|
function dispatchSee(problem, consequence, extras, kind) {
|
|
1247
1374
|
if (!_server) {
|
|
1248
1375
|
console.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
|
|
@@ -1259,10 +1386,16 @@ var see = Object.assign(
|
|
|
1259
1386
|
);
|
|
1260
1387
|
export {
|
|
1261
1388
|
ANON_ID_COOKIE,
|
|
1389
|
+
Client,
|
|
1390
|
+
Engine,
|
|
1262
1391
|
FLAG_REASONS,
|
|
1263
|
-
FlagsClient,
|
|
1264
1392
|
_murmur3ForTests,
|
|
1393
|
+
_resetConfigureForTests,
|
|
1265
1394
|
_resetShipeasyServerForTests,
|
|
1395
|
+
clearOverrides,
|
|
1396
|
+
configure,
|
|
1397
|
+
configureForOffline,
|
|
1398
|
+
configureForTesting,
|
|
1266
1399
|
configureShipeasyServer,
|
|
1267
1400
|
createInMemoryStickyStore,
|
|
1268
1401
|
fetchLabelsForSSR,
|
|
@@ -1272,6 +1405,10 @@ export {
|
|
|
1272
1405
|
getShipeasyServerClient,
|
|
1273
1406
|
i18n,
|
|
1274
1407
|
isExpected,
|
|
1408
|
+
onChange,
|
|
1409
|
+
overrideConfig,
|
|
1410
|
+
overrideExperiment,
|
|
1411
|
+
overrideFlag,
|
|
1275
1412
|
see,
|
|
1276
1413
|
seeContext,
|
|
1277
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": "
|
|
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": {
|