@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/README.md
CHANGED
|
@@ -1,386 +1,113 @@
|
|
|
1
|
-
|
|
1
|
+
<!--
|
|
2
|
+
This file is GENERATED by scripts/gen-readme.mjs from docs/.
|
|
3
|
+
Do NOT edit by hand — edit the docs, then run: npm run gen:readme
|
|
4
|
+
-->
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
[Shipeasy](https://shipeasy.ai) hosted service.
|
|
6
|
+
# @shipeasy/sdk (TypeScript)
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
[](https://github.com/shipeasy-ai/sdk/actions/workflows/test.yml)
|
|
9
|
+
[](https://www.npmjs.com/package/@shipeasy/sdk)
|
|
10
|
+
[](https://www.npmjs.com/package/@shipeasy/sdk)
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
npm install @shipeasy/sdk
|
|
13
|
-
# or
|
|
14
|
-
pnpm add @shipeasy/sdk
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
For React projects use [`@shipeasy/sdk-react`](https://github.com/shipeasy-ai/sdk-react)
|
|
18
|
-
which wraps this package with a `<ShipeasyProvider>` and hooks.
|
|
19
|
-
|
|
20
|
-
## Quickstart — `configure()` once, then `new Client(user)`
|
|
21
|
-
|
|
22
|
-
The ergonomic front door: **configure once** with your key and an optional
|
|
23
|
-
`attributes` transform from *your own user object* to Shipeasy targeting
|
|
24
|
-
attributes, then evaluate per user with `new Client(user)`. The bound
|
|
25
|
-
`Client` takes **no user argument** on its methods — the user is bound at
|
|
26
|
-
construction.
|
|
27
|
-
|
|
28
|
-
### Browser
|
|
29
|
-
|
|
30
|
-
```ts
|
|
31
|
-
import { configure, Client } from "@shipeasy/sdk/client";
|
|
32
|
-
|
|
33
|
-
// Once, at app startup:
|
|
34
|
-
configure({
|
|
35
|
-
clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY!,
|
|
36
|
-
// Optional — omit when you already pass a plain attribute object.
|
|
37
|
-
attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan }),
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// Per visitor:
|
|
41
|
-
const flags = new Client(currentUser);
|
|
42
|
-
await flags.ready(); // optional — await the first /sdk/evaluate round-trip
|
|
43
|
-
|
|
44
|
-
if (flags.getFlag("new_checkout")) {
|
|
45
|
-
// ship it
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const cfg = flags.getConfig<{ max_uploads: number }>("upload_limits");
|
|
49
|
-
const { params } = flags.getExperiment("hero_cta", { primary_label: "Sign up" });
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
The browser is single-user: `new Client(user)` runs the transform and
|
|
53
|
-
`identify()`s the result under the hood (merging browser context — `locale`,
|
|
54
|
-
`timezone`, `path`, `referrer`, `screen_*`, `user_agent` — and a persisted
|
|
55
|
-
`anonymous_id`). `getFlag` reflects the latest identify; `await client.ready()`
|
|
56
|
-
when you need the first evaluation to have resolved.
|
|
57
|
-
|
|
58
|
-
### Server (Node, Cloudflare Worker, Deno)
|
|
59
|
-
|
|
60
|
-
```ts
|
|
61
|
-
import { configure, Client } from "@shipeasy/sdk/server";
|
|
62
|
-
|
|
63
|
-
// Once, at app boot:
|
|
64
|
-
configure({
|
|
65
|
-
apiKey: process.env.SHIPEASY_SERVER_KEY!,
|
|
66
|
-
attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan, country: u.geo.country }),
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// Per request:
|
|
70
|
-
const flags = new Client(req.user);
|
|
71
|
-
if (flags.getFlag("new_checkout")) { /* ... */ }
|
|
72
|
-
const cfg = flags.getConfig("plan_limits");
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
When you don't pass `attributes`, the transform is the **identity** function —
|
|
76
|
-
the user object you pass is used verbatim, so it should already be the
|
|
77
|
-
attribute bag (`{ user_id, anonymous_id, ...targeting }`).
|
|
12
|
+
The Shipeasy SDK for **feature flags, dynamic configs, kill switches, A/B
|
|
13
|
+
experiments, metric tracking, and SSR/i18n** — for Node, Next.js, and Cloudflare
|
|
14
|
+
Workers. Server-key only on the server; the browser uses its own client key.
|
|
78
15
|
|
|
79
|
-
|
|
16
|
+
> 📚 **Full documentation:** **<https://shipeasy-ai.github.io/sdk/>** — also browsable under
|
|
17
|
+
> [`docs/`](https://github.com/shipeasy-ai/sdk/blob/main/docs). This README is generated from those docs.
|
|
80
18
|
|
|
81
|
-
|
|
82
|
-
heavyweight class that owns the key, HTTP, the blob cache, and the poll timer —
|
|
83
|
-
**renamed from `FlagsClient` / `FlagsClientBrowser` in 6.0.0**). You can still
|
|
84
|
-
construct and drive an `Engine` yourself:
|
|
19
|
+
## 🤖 Using an AI agent?
|
|
85
20
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const engine = new Engine({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
|
|
90
|
-
await engine.initOnce();
|
|
91
|
-
const on = engine.getFlag("new_checkout", { user_id: "u-1", plan: "pro" });
|
|
92
|
-
```
|
|
21
|
+
This SDK ships an installable **agent skill** — a copy-paste-ready guide to
|
|
22
|
+
`configure()` + `new Client(user)`, testing, experiments, error reporting, and
|
|
23
|
+
more, with links the agent can pull for deeper docs:
|
|
93
24
|
|
|
94
|
-
|
|
25
|
+
- **Skill:** [`docs/skill/SKILL.md`](https://github.com/shipeasy-ai/sdk/blob/main/docs/skill/SKILL.md) · raw:
|
|
26
|
+
`https://shipeasy-ai.github.io/sdk/skill/SKILL.md`
|
|
27
|
+
- **Install it** (ships with the package — no network):
|
|
28
|
+
`npx shipeasy-skill install` → `.claude/skills/shipeasy-typescript/SKILL.md`
|
|
29
|
+
(or via the Shipeasy CLI: `shipeasy docs skill --sdk typescript --install`)
|
|
95
30
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
reaches the browser).
|
|
31
|
+
**Humans:** you can copy that skill straight into your own project's agent skills
|
|
32
|
+
directory (e.g. `.claude/skills/shipeasy-typescript/SKILL.md`) so your coding agent
|
|
33
|
+
always uses the correct Shipeasy patterns. Every doc page and snippet is also
|
|
34
|
+
fetchable by URL — start from the manifest at `https://shipeasy-ai.github.io/sdk/manifest.json`.
|
|
101
35
|
|
|
102
|
-
|
|
103
|
-
// app/layout.tsx — Next.js root layout (React Server Component)
|
|
104
|
-
import { shipeasy } from "@shipeasy/sdk/server";
|
|
36
|
+
## Install
|
|
105
37
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return (
|
|
113
|
-
<html>
|
|
114
|
-
<body>
|
|
115
|
-
{/* Render REAL <script> elements — scripts set via
|
|
116
|
-
dangerouslySetInnerHTML do NOT execute. */}
|
|
117
|
-
<script src={boot.bootstrap.src} {...boot.bootstrap.attrs} />
|
|
118
|
-
{boot.i18nLoader && <script src={boot.i18nLoader.src} {...boot.i18nLoader.attrs} />}
|
|
119
|
-
{children}
|
|
120
|
-
</body>
|
|
121
|
-
</html>
|
|
122
|
-
);
|
|
123
|
-
}
|
|
38
|
+
```bash
|
|
39
|
+
npm install @shipeasy/sdk
|
|
40
|
+
# or
|
|
41
|
+
pnpm add @shipeasy/sdk
|
|
42
|
+
# or
|
|
43
|
+
yarn add @shipeasy/sdk
|
|
124
44
|
```
|
|
125
45
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
persists the `__se_anon_id` cookie so the browser buckets **identically** to the
|
|
129
|
-
server. The i18n loader tag carries the SSR strings (`data-strings`) for a
|
|
130
|
-
no-flash first paint plus the public client key for runtime revalidation.
|
|
46
|
+
Per-framework setup (Next.js / Express / Cloudflare Workers) and the anon-id
|
|
47
|
+
middleware are on the [Installation](https://github.com/shipeasy-ai/sdk/blob/main/docs/pages/installation.md) page.
|
|
131
48
|
|
|
132
|
-
|
|
133
|
-
same two tags as an HTML string you can drop straight into the served markup.
|
|
134
|
-
|
|
135
|
-
## Error tracking — `see()`
|
|
136
|
-
|
|
137
|
-
`see` (shipeasy error) is the structured error reporter — opes-style: every
|
|
138
|
-
handled exception documents its **product consequence**, not just its stack.
|
|
139
|
-
Works in vanilla JS on both sides; the whole grammar hangs off one import:
|
|
49
|
+
## Quickstart — `configure()` once, then `new Client(user)` per request
|
|
140
50
|
|
|
141
51
|
```ts
|
|
142
|
-
import {
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
await submitOrder(order);
|
|
146
|
-
} catch (e) {
|
|
147
|
-
see(e).causes_the("checkout").to("use cached prices").extras({ order_id: order.id });
|
|
148
|
-
}
|
|
52
|
+
import { configure, Client } from "@shipeasy/sdk/server"; // or /client
|
|
149
53
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (rows.length > LIMIT) {
|
|
153
|
-
see.Violation("large query")
|
|
154
|
-
.causes_the("search results")
|
|
155
|
-
.to("be trimmed")
|
|
156
|
-
.extras({ rows: rows.length });
|
|
157
|
-
}
|
|
54
|
+
configure({ apiKey: process.env.SHIPEASY_SERVER_KEY!,
|
|
55
|
+
attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan }) });
|
|
158
56
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
return decodeFoo(blob);
|
|
163
|
-
} catch (e) {
|
|
164
|
-
see.ControlFlowException(e).because("because it wasn't an encoded Foo");
|
|
165
|
-
return decodeBar(blob);
|
|
166
|
-
}
|
|
57
|
+
const flags = new Client(currentUser);
|
|
58
|
+
if (flags.getFlag("new_checkout")) { /* ship it */ }
|
|
167
59
|
```
|
|
168
60
|
|
|
169
|
-
|
|
170
|
-
(open / resolved / ignored, regression auto-reopens) with a near-real-time
|
|
171
|
-
occurrence timeseries. The chain dispatches on the next microtask — no
|
|
172
|
-
`.send()` — and ships immediately (`sendBeacon` in the browser, fire-and-forget
|
|
173
|
-
`fetch` on the server), spam-guarded by a 30s dedup window and a per-session
|
|
174
|
-
cap.
|
|
175
|
-
|
|
176
|
-
The client SDK also auto-captures **network failures** (fetch network errors +
|
|
177
|
-
5xx) into the same primitive (`autoCollect: { errors }`, on by default) — each
|
|
178
|
-
names a specific endpoint and a specific outcome. It deliberately does **not**
|
|
179
|
-
blanket-report uncaught exceptions or unhandled promise rejections: those carry
|
|
180
|
-
no actionable consequence ("the page hit an error" names the plumbing, not the
|
|
181
|
-
feature). Code that knows the consequence reports it explicitly with `see()` at
|
|
182
|
-
the catch site.
|
|
183
|
-
|
|
184
|
-
**Rules**: if you don't know the consequence, don't catch the exception. You
|
|
185
|
-
**may** `see()` then re-throw — the re-thrown error links to its inner report
|
|
186
|
-
as a `caused_by` chain instead of double-counting. Never use `see.Violation()`
|
|
187
|
-
for a caught exception (you'd drop the stack). No PII or high-cardinality data
|
|
188
|
-
in extras.
|
|
61
|
+
Constructing `new Client(user)` before `configure()` throws.
|
|
189
62
|
|
|
190
|
-
##
|
|
191
|
-
|
|
192
|
-
```html
|
|
193
|
-
<script
|
|
194
|
-
src="https://cdn.shipeasy.ai/sdk/loader.js"
|
|
195
|
-
data-sdk-key="sdk_client_..."
|
|
196
|
-
data-user-id="user-123"
|
|
197
|
-
data-user-email="u@x.com"
|
|
198
|
-
data-user-plan="pro"
|
|
199
|
-
data-attrs='{"country":"US"}'
|
|
200
|
-
defer
|
|
201
|
-
></script>
|
|
202
|
-
<script>
|
|
203
|
-
await window.shipeasy.ready;
|
|
204
|
-
if (window.shipeasy.getFlag("new_checkout")) { /* … */ }
|
|
205
|
-
</script>
|
|
206
|
-
```
|
|
63
|
+
## Documentation
|
|
207
64
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
`
|
|
65
|
+
| Page | What |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| [Overview](https://github.com/shipeasy-ai/sdk/blob/main/docs/pages/overview.md) | The `configure()` + `new Client(user)` model. |
|
|
68
|
+
| [Installation](https://github.com/shipeasy-ai/sdk/blob/main/docs/pages/installation.md) | Install, frameworks (Next / Express / Workers), `configure()` wiring. |
|
|
69
|
+
| [Configuration](https://github.com/shipeasy-ai/sdk/blob/main/docs/pages/configuration.md) | Keys, `attributes`, one-shot vs poll, every option. |
|
|
70
|
+
| [Feature flags](https://github.com/shipeasy-ai/sdk/blob/main/docs/pages/flags.md) | `getFlag`, `getFlagDetail`, defaults. |
|
|
71
|
+
| [Dynamic configs](https://github.com/shipeasy-ai/sdk/blob/main/docs/pages/configs.md) | `getConfig`, typed decode, defaults. |
|
|
72
|
+
| [Kill switches](https://github.com/shipeasy-ai/sdk/blob/main/docs/pages/killswitches.md) | `getKillswitch`, named switches. |
|
|
73
|
+
| [Experiments](https://github.com/shipeasy-ai/sdk/blob/main/docs/pages/experiments.md) | `getExperiment`, `logExposure`, `track`. |
|
|
74
|
+
| [Internationalization](https://github.com/shipeasy-ai/sdk/blob/main/docs/pages/i18n.md) | SSR bootstrap + i18n loader tags. |
|
|
75
|
+
| [Error reporting](https://github.com/shipeasy-ai/sdk/blob/main/docs/pages/error-reporting.md) | `see()` structured error reporting. |
|
|
76
|
+
| [Testing](https://github.com/shipeasy-ai/sdk/blob/main/docs/pages/testing.md) | `configureForTesting` / `configureForOffline`, overrides. |
|
|
77
|
+
| [OpenFeature](https://github.com/shipeasy-ai/sdk/blob/main/docs/pages/openfeature.md) | `ShipeasyProvider` (OpenFeature server provider). |
|
|
78
|
+
| [Advanced](https://github.com/shipeasy-ai/sdk/blob/main/docs/pages/advanced.md) | Anon-id middleware, private attributes, sticky bucketing, SSR. |
|
|
79
|
+
|
|
80
|
+
Copy-paste snippets live under [`docs/snippets/`](https://github.com/shipeasy-ai/sdk/blob/main/docs/snippets)
|
|
81
|
+
(release · metrics · i18n · ops); an installable agent skill is at
|
|
82
|
+
[`docs/skill/SKILL.md`](https://github.com/shipeasy-ai/sdk/blob/main/docs/skill/SKILL.md).
|
|
211
83
|
|
|
212
84
|
## Testing
|
|
213
85
|
|
|
214
|
-
For unit tests,
|
|
215
|
-
entity with local overrides (Statsig-style). In test mode the client is already
|
|
216
|
-
"initialized"/"ready", `init()`/`initOnce()`/`identify()` are no-ops (they never
|
|
217
|
-
fetch), `track()` is a no-op, telemetry is disabled, and no SDK key is required —
|
|
218
|
-
so your tests never touch the network.
|
|
86
|
+
For unit tests, swap the live `configure()` for **`configureForTesting()`** — a drop-in sibling with **no network, ever** (no SDK key required). It replaces the active configuration with a network-free engine, seeds the values your code should see, and is read through the ordinary `new Client(user)`. In this mode the rules never fetch, `track()` / `logExposure()` are no-ops, and telemetry is off — your tests never touch the network.
|
|
219
87
|
|
|
220
88
|
```ts
|
|
221
|
-
|
|
222
|
-
import { Engine } from "@shipeasy/sdk/server";
|
|
89
|
+
import { configureForTesting, Client, clearOverrides } from "@shipeasy/sdk/server"; // or /client
|
|
223
90
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
client.getFlag("new_checkout", { user_id: "u1" }); // true
|
|
231
|
-
client.getConfig("upload_limits"); // { max_uploads: 50 }
|
|
232
|
-
client.getExperiment("hero_cta", { user_id: "u1" }, { primary_label: "Sign up" });
|
|
233
|
-
// → { inExperiment: true, group: "treatment", params: { primary_label: "Buy now" } }
|
|
234
|
-
|
|
235
|
-
client.track("u1", "purchase"); // no-op — never hits the network
|
|
236
|
-
client.clearOverrides(); // reset every override back to default
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
```ts
|
|
240
|
-
// Browser (vanilla JS — no React required)
|
|
241
|
-
import { Engine } from "@shipeasy/sdk/client";
|
|
242
|
-
|
|
243
|
-
const client = Engine.forTesting();
|
|
244
|
-
|
|
245
|
-
client.overrideFlag("new_checkout", true);
|
|
246
|
-
client.overrideConfig("upload_limits", { max_uploads: 50 });
|
|
247
|
-
client.overrideExperiment("hero_cta", "treatment", { primary_label: "Buy now" });
|
|
248
|
-
|
|
249
|
-
client.getFlag("new_checkout"); // true
|
|
250
|
-
client.getConfig("upload_limits"); // { max_uploads: 50 }
|
|
251
|
-
client.getExperiment("hero_cta", { primary_label: "Sign up" });
|
|
252
|
-
// → { inExperiment: true, group: "treatment", params: { primary_label: "Buy now" } }
|
|
253
|
-
|
|
254
|
-
client.track("purchase"); // no-op
|
|
255
|
-
client.clearOverrides();
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
The `override*` setters also work on a **normal** client (not just `forTesting()`),
|
|
259
|
-
mirroring Statsig — a programmatic override always wins over the fetched values.
|
|
260
|
-
In the browser the precedence is: programmatic override > URL/devtools override
|
|
261
|
-
(`?se_gate_…` / `?se_config_…` / `?se_exp_…`) > the server's evaluation.
|
|
262
|
-
|
|
263
|
-
**API:**
|
|
264
|
-
|
|
265
|
-
```ts
|
|
266
|
-
overrideFlag(name: string, value: boolean): void;
|
|
267
|
-
overrideConfig(name: string, value: unknown): void;
|
|
268
|
-
overrideExperiment(name: string, group: string, params: Record<string, unknown>): void;
|
|
269
|
-
clearOverrides(): void;
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
## Default values
|
|
273
|
-
|
|
274
|
-
`getFlag` / `getConfig` take a caller-supplied default that is returned **only
|
|
275
|
-
when the value can't be evaluated** — the client isn't initialized yet, or the
|
|
276
|
-
key isn't in the loaded rules. A flag that legitimately evaluates to `false`
|
|
277
|
-
(disabled, rule denied, rolled out to 0%) still returns `false`, never the
|
|
278
|
-
default.
|
|
279
|
-
|
|
280
|
-
```ts
|
|
281
|
-
// Server
|
|
282
|
-
flags.get("new_checkout", { user_id: "u1" }); // false for a missing flag
|
|
283
|
-
flags.get("new_checkout", { user_id: "u1" }, true); // true only if not-ready/not-found
|
|
284
|
-
flags.getConfig("limits", { defaultValue: { max: 50 } }); // default when key absent
|
|
285
|
-
|
|
286
|
-
// Browser (no user arg)
|
|
287
|
-
client.getFlag("new_checkout"); // false for a missing flag
|
|
288
|
-
client.getFlag("new_checkout", true); // default when not-ready/not-found
|
|
289
|
-
client.getConfig("limits", { defaultValue: { max: 50 } });
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
The legacy `getConfig(name, decode)` callback signature still works unchanged;
|
|
293
|
-
the options object (`{ decode?, defaultValue? }`) is the new, additive form.
|
|
294
|
-
|
|
295
|
-
## Evaluation detail
|
|
296
|
-
|
|
297
|
-
`getFlagDetail(name[, user])` returns `{ value, reason }` (LaunchDarkly
|
|
298
|
-
`variationDetail` parity) so you can see *why* a flag resolved the way it did:
|
|
299
|
-
|
|
300
|
-
```ts
|
|
301
|
-
import type { FlagReason } from "@shipeasy/sdk/server"; // or /client
|
|
302
|
-
|
|
303
|
-
const d = client.getFlagDetail("new_checkout", { user_id: "u1" });
|
|
304
|
-
// → { value: true, reason: "RULE_MATCH" }
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
`FlagReason` is one of:
|
|
308
|
-
|
|
309
|
-
| reason | meaning |
|
|
310
|
-
| ------------------ | ---------------------------------------------------------- |
|
|
311
|
-
| `CLIENT_NOT_READY` | no rules loaded yet (`init()` / `identify()` pending) |
|
|
312
|
-
| `FLAG_NOT_FOUND` | the gate name isn't in the loaded rules |
|
|
313
|
-
| `OFF` | the gate exists but is disabled / killed (server only) |
|
|
314
|
-
| `OVERRIDE` | a local override (or `?se_gate_…` URL override) decided it |
|
|
315
|
-
| `RULE_MATCH` | the gate evaluated `true` |
|
|
316
|
-
| `DEFAULT` | the gate evaluated `false` |
|
|
317
|
-
|
|
318
|
-
`getFlag` is implemented on top of `getFlagDetail` (single evaluation, single
|
|
319
|
-
telemetry beacon). On the browser the server pre-evaluates the gate's
|
|
320
|
-
enabled/killed state into a boolean, so `OFF` folds into `DEFAULT` there.
|
|
321
|
-
|
|
322
|
-
## Change listeners
|
|
323
|
-
|
|
324
|
-
The server `Engine` fires registered listeners after a **background poll
|
|
325
|
-
returns new data** (HTTP 200, not 304) — handy for invalidating a cache or
|
|
326
|
-
re-rendering when a flag flips. Returns an unsubscribe function. Never fires in
|
|
327
|
-
test/offline mode (no polling happens):
|
|
328
|
-
|
|
329
|
-
```ts
|
|
330
|
-
const client = new Engine({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
|
|
331
|
-
await client.init();
|
|
332
|
-
const unsubscribe = client.onChange(() => {
|
|
333
|
-
console.log("flag rules changed — re-evaluating");
|
|
91
|
+
// Seed everything the code under test should see (no key, no network):
|
|
92
|
+
configureForTesting({
|
|
93
|
+
flags: { new_checkout: true },
|
|
94
|
+
configs: { upload_limits: { max_uploads: 50 } },
|
|
95
|
+
experiments: { hero_cta: ["treatment", { primary_label: "Buy now" }] },
|
|
334
96
|
});
|
|
335
|
-
// later…
|
|
336
|
-
unsubscribe();
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
The browser `Engine` already exposes the equivalent `subscribe()`
|
|
340
|
-
(fires after each `identify()` / override change).
|
|
341
97
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
two SDK wire bodies:
|
|
348
|
-
|
|
349
|
-
```jsonc
|
|
350
|
-
// snapshot.json
|
|
351
|
-
{
|
|
352
|
-
"flags": /* body of GET /sdk/flags */,
|
|
353
|
-
"experiments": /* body of GET /sdk/experiments */
|
|
354
|
-
}
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
```ts
|
|
358
|
-
import { Engine } from "@shipeasy/sdk/server";
|
|
359
|
-
|
|
360
|
-
const client = Engine.fromFile("./snapshot.json");
|
|
361
|
-
// or, if you already hold the parsed object:
|
|
362
|
-
const client = Engine.fromSnapshot({ flags, experiments });
|
|
98
|
+
const flags = new Client({ user_id: "u_1" }); // construct once per callsite
|
|
99
|
+
flags.getFlag("new_checkout"); // true
|
|
100
|
+
flags.getConfig("upload_limits"); // { max_uploads: 50 }
|
|
101
|
+
flags.getExperiment("hero_cta", { primary_label: "Sign up" });
|
|
102
|
+
// → { inExperiment: true, group: "treatment", params: { primary_label: "Buy now" } }
|
|
363
103
|
|
|
364
|
-
|
|
104
|
+
clearOverrides(); // reset every seeded override back to the empty-blob default
|
|
365
105
|
```
|
|
366
106
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
## Devtools overlay
|
|
371
|
-
|
|
372
|
-
Press `Shift+Alt+S` on any page running the SDK (or append `?se=1` to the
|
|
373
|
-
URL). The Shipeasy devtools panel mounts in a Shadow DOM overlay and lets
|
|
374
|
-
you flip every gate / config / experiment / translation **for the current
|
|
375
|
-
session only** — handy for QA, demos, and bug repro.
|
|
376
|
-
|
|
377
|
-
## Documentation
|
|
378
|
-
|
|
379
|
-
Full docs at [docs.shipeasy.ai](https://docs.shipeasy.ai). API surfaces
|
|
380
|
-
covered there: targeting rules, holdouts, sequential stats, custom
|
|
381
|
-
metrics, Slack digests, OAuth/SSO, Claude/MCP integration.
|
|
107
|
+
More — the on-the-spot override helpers and a working example snapshot file — on
|
|
108
|
+
the [Testing](https://github.com/shipeasy-ai/sdk/blob/main/docs/pages/testing.md) page.
|
|
382
109
|
|
|
383
110
|
## License
|
|
384
111
|
|
|
385
|
-
[
|
|
386
|
-
|
|
112
|
+
See [LICENSE](https://github.com/shipeasy-ai/sdk/blob/main/LICENSE). Evaluation is tested against the cross-language
|
|
113
|
+
MurmurHash3 vectors in `experiment-platform/04-evaluation.md`.
|
package/dist/client/index.d.mts
CHANGED
|
@@ -654,6 +654,20 @@ declare class Client<U = unknown> {
|
|
|
654
654
|
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
655
655
|
/** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
|
|
656
656
|
getKillswitch(name: string, switchKey?: string): boolean;
|
|
657
|
+
/**
|
|
658
|
+
* Record a conversion/metric event for the bound (identified) user. Delegates
|
|
659
|
+
* to {@link Engine.track} — so an experiment is end-to-end Client-only (no need
|
|
660
|
+
* to drop down to the Engine to log a conversion). Fire-and-forget; no-op in
|
|
661
|
+
* test mode.
|
|
662
|
+
*/
|
|
663
|
+
track(eventName: string, props?: Record<string, unknown>): void;
|
|
664
|
+
/**
|
|
665
|
+
* Log an exposure for `name` at the treatment's render for the bound user.
|
|
666
|
+
* Delegates to {@link Engine.logExposure} (no-op when the visitor isn't
|
|
667
|
+
* enrolled). Pair with `getExperiment(name, …, { logExposure: false })` /
|
|
668
|
+
* `disableAutoExposure` to log exposure exactly when you render.
|
|
669
|
+
*/
|
|
670
|
+
logExposure(name: string): void;
|
|
657
671
|
}
|
|
658
672
|
interface SeeApi {
|
|
659
673
|
/**
|
package/dist/client/index.d.ts
CHANGED
|
@@ -654,6 +654,20 @@ declare class Client<U = unknown> {
|
|
|
654
654
|
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
655
655
|
/** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
|
|
656
656
|
getKillswitch(name: string, switchKey?: string): boolean;
|
|
657
|
+
/**
|
|
658
|
+
* Record a conversion/metric event for the bound (identified) user. Delegates
|
|
659
|
+
* to {@link Engine.track} — so an experiment is end-to-end Client-only (no need
|
|
660
|
+
* to drop down to the Engine to log a conversion). Fire-and-forget; no-op in
|
|
661
|
+
* test mode.
|
|
662
|
+
*/
|
|
663
|
+
track(eventName: string, props?: Record<string, unknown>): void;
|
|
664
|
+
/**
|
|
665
|
+
* Log an exposure for `name` at the treatment's render for the bound user.
|
|
666
|
+
* Delegates to {@link Engine.logExposure} (no-op when the visitor isn't
|
|
667
|
+
* enrolled). Pair with `getExperiment(name, …, { logExposure: false })` /
|
|
668
|
+
* `disableAutoExposure` to log exposure exactly when you render.
|
|
669
|
+
*/
|
|
670
|
+
logExposure(name: string): void;
|
|
657
671
|
}
|
|
658
672
|
interface SeeApi {
|
|
659
673
|
/**
|
package/dist/client/index.js
CHANGED
|
@@ -1662,6 +1662,24 @@ var Client = class {
|
|
|
1662
1662
|
getKillswitch(name, switchKey) {
|
|
1663
1663
|
return this.engine.getKillswitch(name, switchKey);
|
|
1664
1664
|
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Record a conversion/metric event for the bound (identified) user. Delegates
|
|
1667
|
+
* to {@link Engine.track} — so an experiment is end-to-end Client-only (no need
|
|
1668
|
+
* to drop down to the Engine to log a conversion). Fire-and-forget; no-op in
|
|
1669
|
+
* test mode.
|
|
1670
|
+
*/
|
|
1671
|
+
track(eventName, props) {
|
|
1672
|
+
this.engine.track(eventName, props);
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* Log an exposure for `name` at the treatment's render for the bound user.
|
|
1676
|
+
* Delegates to {@link Engine.logExposure} (no-op when the visitor isn't
|
|
1677
|
+
* enrolled). Pair with `getExperiment(name, …, { logExposure: false })` /
|
|
1678
|
+
* `disableAutoExposure` to log exposure exactly when you render.
|
|
1679
|
+
*/
|
|
1680
|
+
logExposure(name) {
|
|
1681
|
+
this.engine.logExposure(name);
|
|
1682
|
+
}
|
|
1665
1683
|
};
|
|
1666
1684
|
function dispatchSee(problem, consequence, extras, kind) {
|
|
1667
1685
|
if (!_client) {
|
package/dist/client/index.mjs
CHANGED
|
@@ -1610,6 +1610,24 @@ var Client = class {
|
|
|
1610
1610
|
getKillswitch(name, switchKey) {
|
|
1611
1611
|
return this.engine.getKillswitch(name, switchKey);
|
|
1612
1612
|
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Record a conversion/metric event for the bound (identified) user. Delegates
|
|
1615
|
+
* to {@link Engine.track} — so an experiment is end-to-end Client-only (no need
|
|
1616
|
+
* to drop down to the Engine to log a conversion). Fire-and-forget; no-op in
|
|
1617
|
+
* test mode.
|
|
1618
|
+
*/
|
|
1619
|
+
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);
|
|
1630
|
+
}
|
|
1613
1631
|
};
|
|
1614
1632
|
function dispatchSee(problem, consequence, extras, kind) {
|
|
1615
1633
|
if (!_client) {
|
|
@@ -318,12 +318,25 @@ declare class Engine {
|
|
|
318
318
|
* synchronous (the methods are `async` only to satisfy the server contract).
|
|
319
319
|
*/
|
|
320
320
|
declare class ShipeasyProvider implements Provider {
|
|
321
|
-
private readonly client;
|
|
322
321
|
readonly metadata: {
|
|
323
322
|
readonly name: "shipeasy";
|
|
324
323
|
};
|
|
325
324
|
readonly runsOn: "server";
|
|
326
|
-
|
|
325
|
+
private readonly client;
|
|
326
|
+
/**
|
|
327
|
+
* Construct the provider. The **global form** (no argument) resolves the
|
|
328
|
+
* engine built by `configure({ apiKey })`, so the docs build it after
|
|
329
|
+
* configuration without ever naming the `Engine`:
|
|
330
|
+
*
|
|
331
|
+
* ```ts
|
|
332
|
+
* configure({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
|
|
333
|
+
* await OpenFeature.setProviderAndWait(new ShipeasyProvider());
|
|
334
|
+
* ```
|
|
335
|
+
*
|
|
336
|
+
* Passing an explicit `Engine` stays supported for advanced/back-compat use.
|
|
337
|
+
* Throws if no engine is passed and `configure()` has not run.
|
|
338
|
+
*/
|
|
339
|
+
constructor(client?: Engine);
|
|
327
340
|
/** Fetch the rules blob once. The SDK fires `Ready` when this resolves. */
|
|
328
341
|
initialize(): Promise<void>;
|
|
329
342
|
onClose(): Promise<void>;
|
|
@@ -318,12 +318,25 @@ declare class Engine {
|
|
|
318
318
|
* synchronous (the methods are `async` only to satisfy the server contract).
|
|
319
319
|
*/
|
|
320
320
|
declare class ShipeasyProvider implements Provider {
|
|
321
|
-
private readonly client;
|
|
322
321
|
readonly metadata: {
|
|
323
322
|
readonly name: "shipeasy";
|
|
324
323
|
};
|
|
325
324
|
readonly runsOn: "server";
|
|
326
|
-
|
|
325
|
+
private readonly client;
|
|
326
|
+
/**
|
|
327
|
+
* Construct the provider. The **global form** (no argument) resolves the
|
|
328
|
+
* engine built by `configure({ apiKey })`, so the docs build it after
|
|
329
|
+
* configuration without ever naming the `Engine`:
|
|
330
|
+
*
|
|
331
|
+
* ```ts
|
|
332
|
+
* configure({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
|
|
333
|
+
* await OpenFeature.setProviderAndWait(new ShipeasyProvider());
|
|
334
|
+
* ```
|
|
335
|
+
*
|
|
336
|
+
* Passing an explicit `Engine` stays supported for advanced/back-compat use.
|
|
337
|
+
* Throws if no engine is passed and `configure()` has not run.
|
|
338
|
+
*/
|
|
339
|
+
constructor(client?: Engine);
|
|
327
340
|
/** Fetch the rules blob once. The SDK fires `Ready` when this resolves. */
|
|
328
341
|
initialize(): Promise<void>;
|
|
329
342
|
onClose(): Promise<void>;
|