@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/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
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
type LogLevel = "silent" | "error" | "warn" | "info" | "debug";
|
|
2
|
+
/** All accepted {@link LogLevel} values, in increasing verbosity. */
|
|
3
|
+
declare const LOG_LEVELS: readonly LogLevel[];
|
|
4
|
+
|
|
1
5
|
type SeeExtras = Record<string, string | number | boolean | null | undefined>;
|
|
2
6
|
type SeeKind = "caught" | "uncaught" | "unhandled_rejection" | "network" | "violation";
|
|
3
7
|
/** Built by `causesThe(subject).to(outcome)` — never constructed by hand. */
|
|
@@ -232,6 +236,14 @@ interface EngineOptions {
|
|
|
232
236
|
disableTelemetry?: boolean;
|
|
233
237
|
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
234
238
|
telemetryUrl?: string;
|
|
239
|
+
/**
|
|
240
|
+
* How chatty the SDK is on `console` when it catches an error internally.
|
|
241
|
+
* Every public runtime method (getFlag/getConfig/getExperiment/getKillswitch/
|
|
242
|
+
* track/logExposure/see) fails silently — returning a safe default instead of
|
|
243
|
+
* throwing — and surfaces the swallowed error through this level. Ordering:
|
|
244
|
+
* `silent` < `error` < `warn` < `info` < `debug`. Defaults to `"warn"`.
|
|
245
|
+
*/
|
|
246
|
+
logLevel?: LogLevel;
|
|
235
247
|
/**
|
|
236
248
|
* Suppress automatic exposure logging in `getExperiment` (Statsig's
|
|
237
249
|
* `disableExposureLogging`). Default false — reading an enrolled variant
|
|
@@ -468,6 +480,13 @@ interface ShipeasyClientConfig {
|
|
|
468
480
|
autoCollect?: boolean | (Partial<AutoCollectGroups> & {
|
|
469
481
|
always?: boolean;
|
|
470
482
|
});
|
|
483
|
+
/**
|
|
484
|
+
* How chatty the SDK is on `console` when it swallows an internal error.
|
|
485
|
+
* `silent` < `error` < `warn` < `info` < `debug`; defaults to `"warn"`. Every
|
|
486
|
+
* runtime read/track/see call fails silently to a safe default and reports the
|
|
487
|
+
* cause at this level. See {@link EngineOptions.logLevel}.
|
|
488
|
+
*/
|
|
489
|
+
logLevel?: LogLevel;
|
|
471
490
|
/**
|
|
472
491
|
* Disable per-evaluation usage telemetry. Telemetry is ON by default — every
|
|
473
492
|
* flag/config/experiment/killswitch read fires one fire-and-forget beacon
|
|
@@ -654,6 +673,20 @@ declare class Client<U = unknown> {
|
|
|
654
673
|
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
655
674
|
/** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
|
|
656
675
|
getKillswitch(name: string, switchKey?: string): boolean;
|
|
676
|
+
/**
|
|
677
|
+
* Record a conversion/metric event for the bound (identified) user. Delegates
|
|
678
|
+
* to {@link Engine.track} — so an experiment is end-to-end Client-only (no need
|
|
679
|
+
* to drop down to the Engine to log a conversion). Fire-and-forget; no-op in
|
|
680
|
+
* test mode.
|
|
681
|
+
*/
|
|
682
|
+
track(eventName: string, props?: Record<string, unknown>): void;
|
|
683
|
+
/**
|
|
684
|
+
* Log an exposure for `name` at the treatment's render for the bound user.
|
|
685
|
+
* Delegates to {@link Engine.logExposure} (no-op when the visitor isn't
|
|
686
|
+
* enrolled). Pair with `getExperiment(name, …, { logExposure: false })` /
|
|
687
|
+
* `disableAutoExposure` to log exposure exactly when you render.
|
|
688
|
+
*/
|
|
689
|
+
logExposure(name: string): void;
|
|
657
690
|
}
|
|
658
691
|
interface SeeApi {
|
|
659
692
|
/**
|
|
@@ -746,4 +779,4 @@ interface I18nFacade {
|
|
|
746
779
|
}
|
|
747
780
|
declare const i18n: I18nFacade;
|
|
748
781
|
|
|
749
|
-
export { type AttributesFn, type AutoCollectGroups, type BootstrapPayload, Client, type ConfigureOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, FLAG_REASONS, type FlagDetail, type FlagReason, type GetConfigOptions, type GetExperimentOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetConfigureForTests, _resetShipeasyForTests, attachDevtools, configure, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
|
|
782
|
+
export { type AttributesFn, type AutoCollectGroups, type BootstrapPayload, Client, type ConfigureOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, FLAG_REASONS, type FlagDetail, type FlagReason, type GetConfigOptions, type GetExperimentOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, LOG_LEVELS, type LabelAttrs, type LogLevel, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetConfigureForTests, _resetShipeasyForTests, attachDevtools, configure, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
type LogLevel = "silent" | "error" | "warn" | "info" | "debug";
|
|
2
|
+
/** All accepted {@link LogLevel} values, in increasing verbosity. */
|
|
3
|
+
declare const LOG_LEVELS: readonly LogLevel[];
|
|
4
|
+
|
|
1
5
|
type SeeExtras = Record<string, string | number | boolean | null | undefined>;
|
|
2
6
|
type SeeKind = "caught" | "uncaught" | "unhandled_rejection" | "network" | "violation";
|
|
3
7
|
/** Built by `causesThe(subject).to(outcome)` — never constructed by hand. */
|
|
@@ -232,6 +236,14 @@ interface EngineOptions {
|
|
|
232
236
|
disableTelemetry?: boolean;
|
|
233
237
|
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
234
238
|
telemetryUrl?: string;
|
|
239
|
+
/**
|
|
240
|
+
* How chatty the SDK is on `console` when it catches an error internally.
|
|
241
|
+
* Every public runtime method (getFlag/getConfig/getExperiment/getKillswitch/
|
|
242
|
+
* track/logExposure/see) fails silently — returning a safe default instead of
|
|
243
|
+
* throwing — and surfaces the swallowed error through this level. Ordering:
|
|
244
|
+
* `silent` < `error` < `warn` < `info` < `debug`. Defaults to `"warn"`.
|
|
245
|
+
*/
|
|
246
|
+
logLevel?: LogLevel;
|
|
235
247
|
/**
|
|
236
248
|
* Suppress automatic exposure logging in `getExperiment` (Statsig's
|
|
237
249
|
* `disableExposureLogging`). Default false — reading an enrolled variant
|
|
@@ -468,6 +480,13 @@ interface ShipeasyClientConfig {
|
|
|
468
480
|
autoCollect?: boolean | (Partial<AutoCollectGroups> & {
|
|
469
481
|
always?: boolean;
|
|
470
482
|
});
|
|
483
|
+
/**
|
|
484
|
+
* How chatty the SDK is on `console` when it swallows an internal error.
|
|
485
|
+
* `silent` < `error` < `warn` < `info` < `debug`; defaults to `"warn"`. Every
|
|
486
|
+
* runtime read/track/see call fails silently to a safe default and reports the
|
|
487
|
+
* cause at this level. See {@link EngineOptions.logLevel}.
|
|
488
|
+
*/
|
|
489
|
+
logLevel?: LogLevel;
|
|
471
490
|
/**
|
|
472
491
|
* Disable per-evaluation usage telemetry. Telemetry is ON by default — every
|
|
473
492
|
* flag/config/experiment/killswitch read fires one fire-and-forget beacon
|
|
@@ -654,6 +673,20 @@ declare class Client<U = unknown> {
|
|
|
654
673
|
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
655
674
|
/** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
|
|
656
675
|
getKillswitch(name: string, switchKey?: string): boolean;
|
|
676
|
+
/**
|
|
677
|
+
* Record a conversion/metric event for the bound (identified) user. Delegates
|
|
678
|
+
* to {@link Engine.track} — so an experiment is end-to-end Client-only (no need
|
|
679
|
+
* to drop down to the Engine to log a conversion). Fire-and-forget; no-op in
|
|
680
|
+
* test mode.
|
|
681
|
+
*/
|
|
682
|
+
track(eventName: string, props?: Record<string, unknown>): void;
|
|
683
|
+
/**
|
|
684
|
+
* Log an exposure for `name` at the treatment's render for the bound user.
|
|
685
|
+
* Delegates to {@link Engine.logExposure} (no-op when the visitor isn't
|
|
686
|
+
* enrolled). Pair with `getExperiment(name, …, { logExposure: false })` /
|
|
687
|
+
* `disableAutoExposure` to log exposure exactly when you render.
|
|
688
|
+
*/
|
|
689
|
+
logExposure(name: string): void;
|
|
657
690
|
}
|
|
658
691
|
interface SeeApi {
|
|
659
692
|
/**
|
|
@@ -746,4 +779,4 @@ interface I18nFacade {
|
|
|
746
779
|
}
|
|
747
780
|
declare const i18n: I18nFacade;
|
|
748
781
|
|
|
749
|
-
export { type AttributesFn, type AutoCollectGroups, type BootstrapPayload, Client, type ConfigureOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, FLAG_REASONS, type FlagDetail, type FlagReason, type GetConfigOptions, type GetExperimentOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetConfigureForTests, _resetShipeasyForTests, attachDevtools, configure, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
|
|
782
|
+
export { type AttributesFn, type AutoCollectGroups, type BootstrapPayload, Client, type ConfigureOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, FLAG_REASONS, type FlagDetail, type FlagReason, type GetConfigOptions, type GetExperimentOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, LOG_LEVELS, type LabelAttrs, type LogLevel, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetConfigureForTests, _resetShipeasyForTests, attachDevtools, configure, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
|