@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 CHANGED
@@ -1,348 +1,113 @@
1
- # @shipeasy/sdk
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
- Feature gates, runtime configs, experiments, and metrics for the
4
- [Shipeasy](https://shipeasy.ai) hosted service.
6
+ # @shipeasy/sdk (TypeScript)
5
7
 
6
- > Source-available under the [Shipeasy-SAL 1.0](./LICENSE). Use it freely
7
- > as a client of Shipeasy. Don't use it to build a competing service.
8
+ [![Tests](https://github.com/shipeasy-ai/sdk/actions/workflows/test.yml/badge.svg)](https://github.com/shipeasy-ai/sdk/actions/workflows/test.yml)
9
+ [![npm](https://img.shields.io/npm/v/@shipeasy/sdk.svg)](https://www.npmjs.com/package/@shipeasy/sdk)
10
+ [![node](https://img.shields.io/node/v/@shipeasy/sdk.svg)](https://www.npmjs.com/package/@shipeasy/sdk)
8
11
 
9
- ## Install
10
-
11
- ```bash
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
- const client = new FlagsClientBrowser({
26
- sdkKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY!,
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
- `identify()` automatically merges browser context (`locale`, `timezone`,
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
- ## Quickstartserver (Node, Cloudflare Worker, Deno)
47
-
48
- ```ts
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
- ## SSR bootstrap (flags on first paint)
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
- Server-render the evaluated flags / configs / experiments into the page so the
59
- browser SDK reads them **synchronously on first paint** — no flash, no extra
60
- round-trip. The `shipeasy()` server handle emits two declarative `<script>`
61
- tags. **No SDK key is embedded** in the bootstrap tag (the server key never
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
- ```tsx
65
- // app/layout.tsx — Next.js root layout (React Server Component)
66
- import { shipeasy } from "@shipeasy/sdk/server";
36
+ ## Install
67
37
 
68
- export default async function RootLayout({ children }: { children: React.ReactNode }) {
69
- const se = await shipeasy({ serverKey: process.env.SHIPEASY_SERVER_KEY ?? "" });
70
- const boot = se.getBootstrapData({
71
- // public client key — lets the i18n loader revalidate strings at runtime
72
- clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY,
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
- `bootstrap.src` is `https://cdn.shipeasy.ai/sdk/bootstrap.js` a static loader
89
- that reads its own `data-*` attributes, hydrates `window.__SE_BOOTSTRAP`, and
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
- For non-React SSR (Express, raw templates), `se.getBootstrapTags()` returns the
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 { see } from "@shipeasy/sdk/client"; // or "@shipeasy/sdk/server"
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
- // Non-exception problems — the name is a stable identifier (it participates
113
- // in the issue fingerprint); all variable data goes in .extras():
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
- // Expected control-flow exceptions: document them, report nothing —
122
- // auto-capture skips marked errors. The reason must start with "because".
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
- Reports land in the Shipeasy **errors** primitive: fingerprint-grouped issues
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
- ## Drop-in `<script>` loader (no bundler)
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
- The loader IIFE is published to a public R2 bucket on every release and
171
- cached for 1y at `loader-vX.Y.Z.js` (immutable) plus a rolling 5-minute
172
- `loader.js`.
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, build a **no-network** client with `forTesting()` and seed every
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 type { FlagReason } from "@shipeasy/sdk/server"; // or /client
89
+ import { configureForTesting, Client, clearOverrides } from "@shipeasy/sdk/server"; // or /client
264
90
 
265
- const d = client.getFlagDetail("new_checkout", { user_id: "u1" });
266
- // → { value: true, reason: "RULE_MATCH" }
267
- ```
268
-
269
- `FlagReason` is one of:
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
- Build a fully offline server client from a captured snapshot zero network.
307
- Evaluations run the real eval against the snapshot; `init()`/`initOnce()`/
308
- `track()` are no-ops and overrides still apply on top. The snapshot is just the
309
- two SDK wire bodies:
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
- client.getFlag("new_checkout", { user_id: "u1" });
104
+ clearOverrides(); // reset every seeded override back to the empty-blob default
327
105
  ```
328
106
 
329
- `fromFile` is Node-only (it reads the file with `node:fs`); `fromSnapshot` works
330
- anywhere.
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
- [Shipeasy-SAL 1.0](./LICENSE) source-available, non-commercial-use,
348
- permitted for use as a Shipeasy client.
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`.
@@ -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 FlagsClientBrowserEnv = "dev" | "staging" | "prod";
206
- interface FlagsClientBrowserOptions {
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?: FlagsClientBrowserEnv;
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 FlagsClientBrowser.forTesting} factory over passing this directly.
262
+ * {@link Engine.forTesting} factory over passing this directly.
263
263
  */
264
264
  testMode?: boolean;
265
265
  }
266
- declare class FlagsClientBrowser {
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: FlagsClientBrowserOptions);
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 = FlagsClientBrowser.forTesting();
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<FlagsClientBrowserOptions>): FlagsClientBrowser;
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: FlagsClientBrowser, opts?: AttachDevtoolsOptions): () => void;
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 FlagsClientBrowserOptions.privateAttributes}.
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 FlagsClientBrowserOptions.stickyBucketing}.
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: FlagsClientBrowserOptions): FlagsClientBrowser;
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(): FlagsClientBrowser | null;
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: FlagsClientBrowserOptions): void;
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 FlagsClientBrowser.logExposure}. No-op before configure(). */
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 Consequence, type ExperimentResult, FLAG_REASONS, type FlagDetail, type FlagReason, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, 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, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
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 };