@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/README.md
CHANGED
|
@@ -1,348 +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 — browser
|
|
21
|
-
|
|
22
|
-
```ts
|
|
23
|
-
import { FlagsClientBrowser } from "@shipeasy/sdk/client";
|
|
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.
|
|
24
15
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
await client.identify({ user_id: "user-123", plan: "pro" });
|
|
30
|
-
|
|
31
|
-
if (client.getFlag("new_checkout")) {
|
|
32
|
-
// ship it
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const cfg = client.getConfig<{ max_uploads: number }>("upload_limits");
|
|
36
|
-
const { params } = client.getExperiment("hero_cta", {
|
|
37
|
-
primary_label: "Sign up",
|
|
38
|
-
});
|
|
39
|
-
```
|
|
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.
|
|
40
18
|
|
|
41
|
-
|
|
42
|
-
`path`, `referrer`, `screen_*`, `user_agent`) and a persisted
|
|
43
|
-
`anonymous_id` into the payload — so gate rules can target by locale or
|
|
44
|
-
holdouts can hash anonymous visitors out of the box.
|
|
19
|
+
## 🤖 Using an AI agent?
|
|
45
20
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
import { FlagsClient } from "@shipeasy/sdk/server";
|
|
50
|
-
|
|
51
|
-
const client = new FlagsClient({ sdkKey: process.env.SHIPEASY_SERVER_KEY! });
|
|
52
|
-
|
|
53
|
-
const cfg = await client.getConfig("plan_limits", { user_id: "u-1" });
|
|
54
|
-
```
|
|
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:
|
|
55
24
|
|
|
56
|
-
|
|
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`)
|
|
57
30
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
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`.
|
|
63
35
|
|
|
64
|
-
|
|
65
|
-
// app/layout.tsx — Next.js root layout (React Server Component)
|
|
66
|
-
import { shipeasy } from "@shipeasy/sdk/server";
|
|
36
|
+
## Install
|
|
67
37
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return (
|
|
75
|
-
<html>
|
|
76
|
-
<body>
|
|
77
|
-
{/* Render REAL <script> elements — scripts set via
|
|
78
|
-
dangerouslySetInnerHTML do NOT execute. */}
|
|
79
|
-
<script src={boot.bootstrap.src} {...boot.bootstrap.attrs} />
|
|
80
|
-
{boot.i18nLoader && <script src={boot.i18nLoader.src} {...boot.i18nLoader.attrs} />}
|
|
81
|
-
{children}
|
|
82
|
-
</body>
|
|
83
|
-
</html>
|
|
84
|
-
);
|
|
85
|
-
}
|
|
38
|
+
```bash
|
|
39
|
+
npm install @shipeasy/sdk
|
|
40
|
+
# or
|
|
41
|
+
pnpm add @shipeasy/sdk
|
|
42
|
+
# or
|
|
43
|
+
yarn add @shipeasy/sdk
|
|
86
44
|
```
|
|
87
45
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
persists the `__se_anon_id` cookie so the browser buckets **identically** to the
|
|
91
|
-
server. The i18n loader tag carries the SSR strings (`data-strings`) for a
|
|
92
|
-
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.
|
|
93
48
|
|
|
94
|
-
|
|
95
|
-
same two tags as an HTML string you can drop straight into the served markup.
|
|
96
|
-
|
|
97
|
-
## Error tracking — `see()`
|
|
98
|
-
|
|
99
|
-
`see` (shipeasy error) is the structured error reporter — opes-style: every
|
|
100
|
-
handled exception documents its **product consequence**, not just its stack.
|
|
101
|
-
Works in vanilla JS on both sides; the whole grammar hangs off one import:
|
|
49
|
+
## Quickstart — `configure()` once, then `new Client(user)` per request
|
|
102
50
|
|
|
103
51
|
```ts
|
|
104
|
-
import {
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
await submitOrder(order);
|
|
108
|
-
} catch (e) {
|
|
109
|
-
see(e).causes_the("checkout").to("use cached prices").extras({ order_id: order.id });
|
|
110
|
-
}
|
|
52
|
+
import { configure, Client } from "@shipeasy/sdk/server"; // or /client
|
|
111
53
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (rows.length > LIMIT) {
|
|
115
|
-
see.Violation("large query")
|
|
116
|
-
.causes_the("search results")
|
|
117
|
-
.to("be trimmed")
|
|
118
|
-
.extras({ rows: rows.length });
|
|
119
|
-
}
|
|
54
|
+
configure({ apiKey: process.env.SHIPEASY_SERVER_KEY!,
|
|
55
|
+
attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan }) });
|
|
120
56
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
return decodeFoo(blob);
|
|
125
|
-
} catch (e) {
|
|
126
|
-
see.ControlFlowException(e).because("because it wasn't an encoded Foo");
|
|
127
|
-
return decodeBar(blob);
|
|
128
|
-
}
|
|
57
|
+
const flags = new Client(currentUser);
|
|
58
|
+
if (flags.getFlag("new_checkout")) { /* ship it */ }
|
|
129
59
|
```
|
|
130
60
|
|
|
131
|
-
|
|
132
|
-
(open / resolved / ignored, regression auto-reopens) with a near-real-time
|
|
133
|
-
occurrence timeseries. The chain dispatches on the next microtask — no
|
|
134
|
-
`.send()` — and ships immediately (`sendBeacon` in the browser, fire-and-forget
|
|
135
|
-
`fetch` on the server), spam-guarded by a 30s dedup window and a per-session
|
|
136
|
-
cap.
|
|
137
|
-
|
|
138
|
-
The client SDK also auto-captures **network failures** (fetch network errors +
|
|
139
|
-
5xx) into the same primitive (`autoCollect: { errors }`, on by default) — each
|
|
140
|
-
names a specific endpoint and a specific outcome. It deliberately does **not**
|
|
141
|
-
blanket-report uncaught exceptions or unhandled promise rejections: those carry
|
|
142
|
-
no actionable consequence ("the page hit an error" names the plumbing, not the
|
|
143
|
-
feature). Code that knows the consequence reports it explicitly with `see()` at
|
|
144
|
-
the catch site.
|
|
145
|
-
|
|
146
|
-
**Rules**: if you don't know the consequence, don't catch the exception. You
|
|
147
|
-
**may** `see()` then re-throw — the re-thrown error links to its inner report
|
|
148
|
-
as a `caused_by` chain instead of double-counting. Never use `see.Violation()`
|
|
149
|
-
for a caught exception (you'd drop the stack). No PII or high-cardinality data
|
|
150
|
-
in extras.
|
|
61
|
+
Constructing `new Client(user)` before `configure()` throws.
|
|
151
62
|
|
|
152
|
-
##
|
|
153
|
-
|
|
154
|
-
```html
|
|
155
|
-
<script
|
|
156
|
-
src="https://cdn.shipeasy.ai/sdk/loader.js"
|
|
157
|
-
data-sdk-key="sdk_client_..."
|
|
158
|
-
data-user-id="user-123"
|
|
159
|
-
data-user-email="u@x.com"
|
|
160
|
-
data-user-plan="pro"
|
|
161
|
-
data-attrs='{"country":"US"}'
|
|
162
|
-
defer
|
|
163
|
-
></script>
|
|
164
|
-
<script>
|
|
165
|
-
await window.shipeasy.ready;
|
|
166
|
-
if (window.shipeasy.getFlag("new_checkout")) { /* … */ }
|
|
167
|
-
</script>
|
|
168
|
-
```
|
|
63
|
+
## Documentation
|
|
169
64
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
`
|
|
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).
|
|
173
83
|
|
|
174
84
|
## Testing
|
|
175
85
|
|
|
176
|
-
For unit tests,
|
|
177
|
-
entity with local overrides (Statsig-style). In test mode the client is already
|
|
178
|
-
"initialized"/"ready", `init()`/`initOnce()`/`identify()` are no-ops (they never
|
|
179
|
-
fetch), `track()` is a no-op, telemetry is disabled, and no SDK key is required —
|
|
180
|
-
so your tests never touch the network.
|
|
181
|
-
|
|
182
|
-
```ts
|
|
183
|
-
// Server (Node / Cloudflare Worker / Deno)
|
|
184
|
-
import { FlagsClient } from "@shipeasy/sdk/server";
|
|
185
|
-
|
|
186
|
-
const client = FlagsClient.forTesting();
|
|
187
|
-
|
|
188
|
-
client.overrideFlag("new_checkout", true);
|
|
189
|
-
client.overrideConfig("upload_limits", { max_uploads: 50 });
|
|
190
|
-
client.overrideExperiment("hero_cta", "treatment", { primary_label: "Buy now" });
|
|
191
|
-
|
|
192
|
-
client.getFlag("new_checkout", { user_id: "u1" }); // true
|
|
193
|
-
client.getConfig("upload_limits"); // { max_uploads: 50 }
|
|
194
|
-
client.getExperiment("hero_cta", { user_id: "u1" }, { primary_label: "Sign up" });
|
|
195
|
-
// → { inExperiment: true, group: "treatment", params: { primary_label: "Buy now" } }
|
|
196
|
-
|
|
197
|
-
client.track("u1", "purchase"); // no-op — never hits the network
|
|
198
|
-
client.clearOverrides(); // reset every override back to default
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
```ts
|
|
202
|
-
// Browser (vanilla JS — no React required)
|
|
203
|
-
import { FlagsClientBrowser } from "@shipeasy/sdk/client";
|
|
204
|
-
|
|
205
|
-
const client = FlagsClientBrowser.forTesting();
|
|
206
|
-
|
|
207
|
-
client.overrideFlag("new_checkout", true);
|
|
208
|
-
client.overrideConfig("upload_limits", { max_uploads: 50 });
|
|
209
|
-
client.overrideExperiment("hero_cta", "treatment", { primary_label: "Buy now" });
|
|
210
|
-
|
|
211
|
-
client.getFlag("new_checkout"); // true
|
|
212
|
-
client.getConfig("upload_limits"); // { max_uploads: 50 }
|
|
213
|
-
client.getExperiment("hero_cta", { primary_label: "Sign up" });
|
|
214
|
-
// → { inExperiment: true, group: "treatment", params: { primary_label: "Buy now" } }
|
|
215
|
-
|
|
216
|
-
client.track("purchase"); // no-op
|
|
217
|
-
client.clearOverrides();
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
The `override*` setters also work on a **normal** client (not just `forTesting()`),
|
|
221
|
-
mirroring Statsig — a programmatic override always wins over the fetched values.
|
|
222
|
-
In the browser the precedence is: programmatic override > URL/devtools override
|
|
223
|
-
(`?se_gate_…` / `?se_config_…` / `?se_exp_…`) > the server's evaluation.
|
|
224
|
-
|
|
225
|
-
**API:**
|
|
226
|
-
|
|
227
|
-
```ts
|
|
228
|
-
overrideFlag(name: string, value: boolean): void;
|
|
229
|
-
overrideConfig(name: string, value: unknown): void;
|
|
230
|
-
overrideExperiment(name: string, group: string, params: Record<string, unknown>): void;
|
|
231
|
-
clearOverrides(): void;
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
## Default values
|
|
235
|
-
|
|
236
|
-
`getFlag` / `getConfig` take a caller-supplied default that is returned **only
|
|
237
|
-
when the value can't be evaluated** — the client isn't initialized yet, or the
|
|
238
|
-
key isn't in the loaded rules. A flag that legitimately evaluates to `false`
|
|
239
|
-
(disabled, rule denied, rolled out to 0%) still returns `false`, never the
|
|
240
|
-
default.
|
|
241
|
-
|
|
242
|
-
```ts
|
|
243
|
-
// Server
|
|
244
|
-
flags.get("new_checkout", { user_id: "u1" }); // false for a missing flag
|
|
245
|
-
flags.get("new_checkout", { user_id: "u1" }, true); // true only if not-ready/not-found
|
|
246
|
-
flags.getConfig("limits", { defaultValue: { max: 50 } }); // default when key absent
|
|
247
|
-
|
|
248
|
-
// Browser (no user arg)
|
|
249
|
-
client.getFlag("new_checkout"); // false for a missing flag
|
|
250
|
-
client.getFlag("new_checkout", true); // default when not-ready/not-found
|
|
251
|
-
client.getConfig("limits", { defaultValue: { max: 50 } });
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
The legacy `getConfig(name, decode)` callback signature still works unchanged;
|
|
255
|
-
the options object (`{ decode?, defaultValue? }`) is the new, additive form.
|
|
256
|
-
|
|
257
|
-
## Evaluation detail
|
|
258
|
-
|
|
259
|
-
`getFlagDetail(name[, user])` returns `{ value, reason }` (LaunchDarkly
|
|
260
|
-
`variationDetail` parity) so you can see *why* a flag resolved the way it did:
|
|
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.
|
|
261
87
|
|
|
262
88
|
```ts
|
|
263
|
-
import
|
|
89
|
+
import { configureForTesting, Client, clearOverrides } from "@shipeasy/sdk/server"; // or /client
|
|
264
90
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
| reason | meaning |
|
|
272
|
-
| ------------------ | ---------------------------------------------------------- |
|
|
273
|
-
| `CLIENT_NOT_READY` | no rules loaded yet (`init()` / `identify()` pending) |
|
|
274
|
-
| `FLAG_NOT_FOUND` | the gate name isn't in the loaded rules |
|
|
275
|
-
| `OFF` | the gate exists but is disabled / killed (server only) |
|
|
276
|
-
| `OVERRIDE` | a local override (or `?se_gate_…` URL override) decided it |
|
|
277
|
-
| `RULE_MATCH` | the gate evaluated `true` |
|
|
278
|
-
| `DEFAULT` | the gate evaluated `false` |
|
|
279
|
-
|
|
280
|
-
`getFlag` is implemented on top of `getFlagDetail` (single evaluation, single
|
|
281
|
-
telemetry beacon). On the browser the server pre-evaluates the gate's
|
|
282
|
-
enabled/killed state into a boolean, so `OFF` folds into `DEFAULT` there.
|
|
283
|
-
|
|
284
|
-
## Change listeners
|
|
285
|
-
|
|
286
|
-
The server `FlagsClient` fires registered listeners after a **background poll
|
|
287
|
-
returns new data** (HTTP 200, not 304) — handy for invalidating a cache or
|
|
288
|
-
re-rendering when a flag flips. Returns an unsubscribe function. Never fires in
|
|
289
|
-
test/offline mode (no polling happens):
|
|
290
|
-
|
|
291
|
-
```ts
|
|
292
|
-
const client = new FlagsClient({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
|
|
293
|
-
await client.init();
|
|
294
|
-
const unsubscribe = client.onChange(() => {
|
|
295
|
-
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" }] },
|
|
296
96
|
});
|
|
297
|
-
// later…
|
|
298
|
-
unsubscribe();
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
The browser `FlagsClientBrowser` already exposes the equivalent `subscribe()`
|
|
302
|
-
(fires after each `identify()` / override change).
|
|
303
|
-
|
|
304
|
-
## Offline snapshot
|
|
305
97
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
```jsonc
|
|
312
|
-
// snapshot.json
|
|
313
|
-
{
|
|
314
|
-
"flags": /* body of GET /sdk/flags */,
|
|
315
|
-
"experiments": /* body of GET /sdk/experiments */
|
|
316
|
-
}
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
```ts
|
|
320
|
-
import { FlagsClient } from "@shipeasy/sdk/server";
|
|
321
|
-
|
|
322
|
-
const client = FlagsClient.fromFile("./snapshot.json");
|
|
323
|
-
// or, if you already hold the parsed object:
|
|
324
|
-
const client = FlagsClient.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" } }
|
|
325
103
|
|
|
326
|
-
|
|
104
|
+
clearOverrides(); // reset every seeded override back to the empty-blob default
|
|
327
105
|
```
|
|
328
106
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
## Devtools overlay
|
|
333
|
-
|
|
334
|
-
Press `Shift+Alt+S` on any page running the SDK (or append `?se=1` to the
|
|
335
|
-
URL). The Shipeasy devtools panel mounts in a Shadow DOM overlay and lets
|
|
336
|
-
you flip every gate / config / experiment / translation **for the current
|
|
337
|
-
session only** — handy for QA, demos, and bug repro.
|
|
338
|
-
|
|
339
|
-
## Documentation
|
|
340
|
-
|
|
341
|
-
Full docs at [docs.shipeasy.ai](https://docs.shipeasy.ai). API surfaces
|
|
342
|
-
covered there: targeting rules, holdouts, sequential stats, custom
|
|
343
|
-
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.
|
|
344
109
|
|
|
345
110
|
## License
|
|
346
111
|
|
|
347
|
-
[
|
|
348
|
-
|
|
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
|
@@ -202,8 +202,8 @@ declare function sameOrigin(rawUrl: string): boolean;
|
|
|
202
202
|
* pass through unchanged (correlation is optional, never breaks the fetch).
|
|
203
203
|
*/
|
|
204
204
|
declare function injectCorrelationHeader(args: Parameters<typeof fetch>, corr: string): Parameters<typeof fetch>;
|
|
205
|
-
type
|
|
206
|
-
interface
|
|
205
|
+
type EngineEnv = "dev" | "staging" | "prod";
|
|
206
|
+
interface EngineOptions {
|
|
207
207
|
sdkKey: string;
|
|
208
208
|
baseUrl?: string;
|
|
209
209
|
autoGuardrails?: boolean;
|
|
@@ -222,7 +222,7 @@ interface FlagsClientBrowserOptions {
|
|
|
222
222
|
*/
|
|
223
223
|
autoCollectAlways?: boolean;
|
|
224
224
|
/** Which published env to read values from. Defaults to "prod". */
|
|
225
|
-
env?:
|
|
225
|
+
env?: EngineEnv;
|
|
226
226
|
/**
|
|
227
227
|
* Per-evaluation usage telemetry. ON by default — each getFlag/getConfig/
|
|
228
228
|
* getExperiment/getKillswitch call fires one fire-and-forget sendBeacon so
|
|
@@ -259,11 +259,11 @@ interface FlagsClientBrowserOptions {
|
|
|
259
259
|
* Test mode — no network at all. identify()/init are no-ops (never call
|
|
260
260
|
* /sdk/evaluate), track() is a no-op, telemetry is forced off, and the client
|
|
261
261
|
* starts "ready" with an empty eval result. Prefer the
|
|
262
|
-
* {@link
|
|
262
|
+
* {@link Engine.forTesting} factory over passing this directly.
|
|
263
263
|
*/
|
|
264
264
|
testMode?: boolean;
|
|
265
265
|
}
|
|
266
|
-
declare class
|
|
266
|
+
declare class Engine {
|
|
267
267
|
private readonly sdkKey;
|
|
268
268
|
private readonly baseUrl;
|
|
269
269
|
private readonly autoGuardrails;
|
|
@@ -288,7 +288,7 @@ declare class FlagsClientBrowser {
|
|
|
288
288
|
private readonly configOverrides;
|
|
289
289
|
private readonly experimentOverrides;
|
|
290
290
|
private onOverrideChange;
|
|
291
|
-
constructor(opts:
|
|
291
|
+
constructor(opts: EngineOptions);
|
|
292
292
|
/**
|
|
293
293
|
* Build a no-network, immediately-usable browser client for tests
|
|
294
294
|
* (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
|
|
@@ -297,12 +297,12 @@ declare class FlagsClientBrowser {
|
|
|
297
297
|
* key required.
|
|
298
298
|
*
|
|
299
299
|
* ```ts
|
|
300
|
-
* const client =
|
|
300
|
+
* const client = Engine.forTesting();
|
|
301
301
|
* client.overrideFlag("new_checkout", true);
|
|
302
302
|
* client.getFlag("new_checkout"); // true
|
|
303
303
|
* ```
|
|
304
304
|
*/
|
|
305
|
-
static forTesting(opts?: Partial<
|
|
305
|
+
static forTesting(opts?: Partial<EngineOptions>): Engine;
|
|
306
306
|
identify(user: User): Promise<void>;
|
|
307
307
|
/**
|
|
308
308
|
* Report a structured error into the errors primitive. Flushes immediately
|
|
@@ -411,7 +411,7 @@ interface AttachDevtoolsOptions {
|
|
|
411
411
|
* each `identify()`/override change. Returns an unsubscribe function for
|
|
412
412
|
* cleanup (e.g. React effect teardown).
|
|
413
413
|
*/
|
|
414
|
-
declare function attachDevtools(client:
|
|
414
|
+
declare function attachDevtools(client: Engine, opts?: AttachDevtoolsOptions): () => void;
|
|
415
415
|
/** Configure the singleton. Idempotent — re-calling with the same opts is a no-op. */
|
|
416
416
|
interface ShipeasyClientConfig {
|
|
417
417
|
/**
|
|
@@ -484,13 +484,13 @@ interface ShipeasyClientConfig {
|
|
|
484
484
|
* Attribute names usable for targeting but never persisted in analytics
|
|
485
485
|
* (LD/Statsig `privateAttributes`). Sent to the edge for evaluation, never
|
|
486
486
|
* stored, and stripped from `flags.track(props)`. See
|
|
487
|
-
* {@link
|
|
487
|
+
* {@link EngineOptions.privateAttributes}.
|
|
488
488
|
*/
|
|
489
489
|
privateAttributes?: string[];
|
|
490
490
|
/**
|
|
491
491
|
* Sticky bucketing (doc 20 §2). ON by default — locks each enrolled unit to
|
|
492
492
|
* its first-assigned variant via the `__se_sticky` cookie. Pass `false` to
|
|
493
|
-
* opt out. See {@link
|
|
493
|
+
* opt out. See {@link EngineOptions.stickyBucketing}.
|
|
494
494
|
*/
|
|
495
495
|
stickyBucketing?: boolean;
|
|
496
496
|
}
|
|
@@ -505,9 +505,9 @@ interface ShipeasyClientConfig {
|
|
|
505
505
|
* A later flags.identify({ user_id }) overrides this in place; anonId stays stable.
|
|
506
506
|
*/
|
|
507
507
|
declare function shipeasy(opts: ShipeasyClientConfig): () => void;
|
|
508
|
-
declare function configureShipeasy(opts:
|
|
508
|
+
declare function configureShipeasy(opts: EngineOptions): Engine;
|
|
509
509
|
/** Returns the configured singleton, or null if configureShipeasy() hasn't run yet. */
|
|
510
|
-
declare function getShipeasyClient():
|
|
510
|
+
declare function getShipeasyClient(): Engine | null;
|
|
511
511
|
/**
|
|
512
512
|
* Test helper — drop the singleton so the next configureShipeasy() builds fresh.
|
|
513
513
|
* Not part of the documented surface; production code should never call this.
|
|
@@ -539,7 +539,7 @@ interface BootstrapPayload {
|
|
|
539
539
|
* importing this in a module that loads before app boot is harmless.
|
|
540
540
|
*/
|
|
541
541
|
declare const flags: {
|
|
542
|
-
configure(opts:
|
|
542
|
+
configure(opts: EngineOptions): void;
|
|
543
543
|
identify(user: User): Promise<void>;
|
|
544
544
|
/**
|
|
545
545
|
* Read a feature gate.
|
|
@@ -554,7 +554,7 @@ declare const flags: {
|
|
|
554
554
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
555
555
|
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decodeOrOpts?: ((raw: unknown) => P) | GetExperimentOptions<P>, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
|
|
556
556
|
/** Manually log an exposure for an enrolled experiment. See
|
|
557
|
-
* {@link
|
|
557
|
+
* {@link Engine.logExposure}. No-op before configure(). */
|
|
558
558
|
logExposure(name: string): void;
|
|
559
559
|
track(eventName: string, props?: Record<string, unknown>): void;
|
|
560
560
|
/**
|
|
@@ -583,6 +583,92 @@ declare const flags: {
|
|
|
583
583
|
/** True once identify() has completed and flags are available. */
|
|
584
584
|
readonly ready: boolean;
|
|
585
585
|
};
|
|
586
|
+
/** Transform YOUR application's user object into Shipeasy targeting attributes. */
|
|
587
|
+
type AttributesFn<U = unknown> = (user: U) => User;
|
|
588
|
+
interface ConfigureOptions<U = unknown> extends Omit<ShipeasyClientConfig, "clientKey"> {
|
|
589
|
+
/** Public client key — the single key the browser side accepts (NEXT_PUBLIC_SHIPEASY_CLIENT_KEY). */
|
|
590
|
+
clientKey: string;
|
|
591
|
+
/**
|
|
592
|
+
* Map your own user object into the attribute bag every flag/experiment
|
|
593
|
+
* evaluation sees. Runs once per `new Client(user)`. Omit when you already
|
|
594
|
+
* pass a plain attribute object (identity transform — the object is used
|
|
595
|
+
* verbatim, so it should carry `user_id` + any targeting attrs).
|
|
596
|
+
*/
|
|
597
|
+
attributes?: AttributesFn<U>;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Configure the SDK once at app boot, then evaluate per user with
|
|
601
|
+
* `new Client(user)`. Builds the process-wide {@link Engine} (the
|
|
602
|
+
* /sdk/evaluate-backed browser client) and registers the `attributes`
|
|
603
|
+
* transform. The first call wins; later calls reuse the existing engine
|
|
604
|
+
* (mirrors {@link shipeasy}).
|
|
605
|
+
*
|
|
606
|
+
* ```ts
|
|
607
|
+
* import { configure, Client } from "@shipeasy/sdk/client";
|
|
608
|
+
*
|
|
609
|
+
* configure({
|
|
610
|
+
* clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY!,
|
|
611
|
+
* attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan }),
|
|
612
|
+
* });
|
|
613
|
+
*
|
|
614
|
+
* const flags = new Client(currentUser);
|
|
615
|
+
* await flags.ready();
|
|
616
|
+
* if (flags.getFlag("new_checkout")) { ... }
|
|
617
|
+
* ```
|
|
618
|
+
*
|
|
619
|
+
* Returns a cleanup function (same as {@link shipeasy}) that removes the
|
|
620
|
+
* devtools listeners — call it on teardown.
|
|
621
|
+
*/
|
|
622
|
+
declare function configure<U = unknown>(opts: ConfigureOptions<U>): () => void;
|
|
623
|
+
/** Test seam: reset the registered attribute transform. */
|
|
624
|
+
declare function _resetConfigureForTests(): void;
|
|
625
|
+
/**
|
|
626
|
+
* A user-bound evaluation handle for the browser. Construct one for the current
|
|
627
|
+
* visitor — it's cheap (it delegates to the single {@link Engine} built by
|
|
628
|
+
* {@link configure}); it does NOT open its own connection. The configured
|
|
629
|
+
* `attributes` transform runs once here and the result is `identify()`-ed into
|
|
630
|
+
* the engine (fire-and-forget, since identify is async).
|
|
631
|
+
*
|
|
632
|
+
* Because the browser is single-user, all bound handles share the engine's
|
|
633
|
+
* latest eval result. Use {@link Client.ready} to await the first evaluation.
|
|
634
|
+
*
|
|
635
|
+
* ```ts
|
|
636
|
+
* const flags = new Client(currentUser);
|
|
637
|
+
* await flags.ready();
|
|
638
|
+
* flags.getFlag("new_checkout"); // no user arg — bound at construction
|
|
639
|
+
* flags.getExperiment("price_test", { price: 9 });
|
|
640
|
+
* ```
|
|
641
|
+
*/
|
|
642
|
+
declare class Client<U = unknown> {
|
|
643
|
+
private readonly engine;
|
|
644
|
+
/** The resolved attribute bag this handle evaluates against. */
|
|
645
|
+
readonly attributes: User;
|
|
646
|
+
private readonly _identify;
|
|
647
|
+
constructor(user: U);
|
|
648
|
+
/** Resolves once the engine's identify() for this user has completed. */
|
|
649
|
+
ready(): Promise<void>;
|
|
650
|
+
getFlag(name: string, defaultValue?: boolean): boolean;
|
|
651
|
+
getFlagDetail(name: string): FlagDetail;
|
|
652
|
+
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
653
|
+
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
654
|
+
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
655
|
+
/** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
|
|
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;
|
|
671
|
+
}
|
|
586
672
|
interface SeeApi {
|
|
587
673
|
/**
|
|
588
674
|
* Report a handled problem and its product consequence:
|
|
@@ -674,4 +760,4 @@ interface I18nFacade {
|
|
|
674
760
|
}
|
|
675
761
|
declare const i18n: I18nFacade;
|
|
676
762
|
|
|
677
|
-
export { type AutoCollectGroups, type BootstrapPayload, type
|
|
763
|
+
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 };
|