@shipeasy/sdk 6.0.0 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,386 +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 — `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
- ### Low-level: the `Engine` directly
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
- `Client` is a cheap, user-bound handle over a single shared `Engine` (the
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
- ```ts
87
- import { Engine } from "@shipeasy/sdk/server";
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
- ## 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`)
95
30
 
96
- Server-render the evaluated flags / configs / experiments into the page so the
97
- browser SDK reads them **synchronously on first paint** — no flash, no extra
98
- round-trip. The `shipeasy()` server handle emits two declarative `<script>`
99
- tags. **No SDK key is embedded** in the bootstrap tag (the server key never
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
- ```tsx
103
- // app/layout.tsx — Next.js root layout (React Server Component)
104
- import { shipeasy } from "@shipeasy/sdk/server";
36
+ ## Install
105
37
 
106
- export default async function RootLayout({ children }: { children: React.ReactNode }) {
107
- const se = await shipeasy({ serverKey: process.env.SHIPEASY_SERVER_KEY ?? "" });
108
- const boot = se.getBootstrapData({
109
- // public client key — lets the i18n loader revalidate strings at runtime
110
- clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY,
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
- `bootstrap.src` is `https://cdn.shipeasy.ai/sdk/bootstrap.js` a static loader
127
- that reads its own `data-*` attributes, hydrates `window.__SE_BOOTSTRAP`, and
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
- For non-React SSR (Express, raw templates), `se.getBootstrapTags()` returns the
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 { see } from "@shipeasy/sdk/client"; // or "@shipeasy/sdk/server"
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
- // Non-exception problems — the name is a stable identifier (it participates
151
- // in the issue fingerprint); all variable data goes in .extras():
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
- // Expected control-flow exceptions: document them, report nothing —
160
- // auto-capture skips marked errors. The reason must start with "because".
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
- Reports land in the Shipeasy **errors** primitive: fingerprint-grouped issues
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
- ## Drop-in `<script>` loader (no bundler)
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
- The loader IIFE is published to a public R2 bucket on every release and
209
- cached for 1y at `loader-vX.Y.Z.js` (immutable) plus a rolling 5-minute
210
- `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).
211
83
 
212
84
  ## Testing
213
85
 
214
- For unit tests, build a **no-network** client with `forTesting()` and seed every
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
- // Server (Node / Cloudflare Worker / Deno)
222
- import { Engine } from "@shipeasy/sdk/server";
89
+ import { configureForTesting, Client, clearOverrides } from "@shipeasy/sdk/server"; // or /client
223
90
 
224
- const client = Engine.forTesting();
225
-
226
- client.overrideFlag("new_checkout", true);
227
- client.overrideConfig("upload_limits", { max_uploads: 50 });
228
- client.overrideExperiment("hero_cta", "treatment", { primary_label: "Buy now" });
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
- ## Offline snapshot
343
-
344
- Build a fully offline server client from a captured snapshot — zero network.
345
- Evaluations run the real eval against the snapshot; `init()`/`initOnce()`/
346
- `track()` are no-ops and overrides still apply on top. The snapshot is just the
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
- client.getFlag("new_checkout", { user_id: "u1" });
104
+ clearOverrides(); // reset every seeded override back to the empty-blob default
365
105
  ```
366
106
 
367
- `fromFile` is Node-only (it reads the file with `node:fs`); `fromSnapshot` works
368
- anywhere.
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
- [Shipeasy-SAL 1.0](./LICENSE) source-available, non-commercial-use,
386
- 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`.
@@ -654,6 +654,20 @@ declare class Client<U = unknown> {
654
654
  getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
655
655
  /** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
656
656
  getKillswitch(name: string, switchKey?: string): boolean;
657
+ /**
658
+ * Record a conversion/metric event for the bound (identified) user. Delegates
659
+ * to {@link Engine.track} — so an experiment is end-to-end Client-only (no need
660
+ * to drop down to the Engine to log a conversion). Fire-and-forget; no-op in
661
+ * test mode.
662
+ */
663
+ track(eventName: string, props?: Record<string, unknown>): void;
664
+ /**
665
+ * Log an exposure for `name` at the treatment's render for the bound user.
666
+ * Delegates to {@link Engine.logExposure} (no-op when the visitor isn't
667
+ * enrolled). Pair with `getExperiment(name, …, { logExposure: false })` /
668
+ * `disableAutoExposure` to log exposure exactly when you render.
669
+ */
670
+ logExposure(name: string): void;
657
671
  }
658
672
  interface SeeApi {
659
673
  /**
@@ -654,6 +654,20 @@ declare class Client<U = unknown> {
654
654
  getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
655
655
  /** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
656
656
  getKillswitch(name: string, switchKey?: string): boolean;
657
+ /**
658
+ * Record a conversion/metric event for the bound (identified) user. Delegates
659
+ * to {@link Engine.track} — so an experiment is end-to-end Client-only (no need
660
+ * to drop down to the Engine to log a conversion). Fire-and-forget; no-op in
661
+ * test mode.
662
+ */
663
+ track(eventName: string, props?: Record<string, unknown>): void;
664
+ /**
665
+ * Log an exposure for `name` at the treatment's render for the bound user.
666
+ * Delegates to {@link Engine.logExposure} (no-op when the visitor isn't
667
+ * enrolled). Pair with `getExperiment(name, …, { logExposure: false })` /
668
+ * `disableAutoExposure` to log exposure exactly when you render.
669
+ */
670
+ logExposure(name: string): void;
657
671
  }
658
672
  interface SeeApi {
659
673
  /**
@@ -1662,6 +1662,24 @@ var Client = class {
1662
1662
  getKillswitch(name, switchKey) {
1663
1663
  return this.engine.getKillswitch(name, switchKey);
1664
1664
  }
1665
+ /**
1666
+ * Record a conversion/metric event for the bound (identified) user. Delegates
1667
+ * to {@link Engine.track} — so an experiment is end-to-end Client-only (no need
1668
+ * to drop down to the Engine to log a conversion). Fire-and-forget; no-op in
1669
+ * test mode.
1670
+ */
1671
+ track(eventName, props) {
1672
+ this.engine.track(eventName, props);
1673
+ }
1674
+ /**
1675
+ * Log an exposure for `name` at the treatment's render for the bound user.
1676
+ * Delegates to {@link Engine.logExposure} (no-op when the visitor isn't
1677
+ * enrolled). Pair with `getExperiment(name, …, { logExposure: false })` /
1678
+ * `disableAutoExposure` to log exposure exactly when you render.
1679
+ */
1680
+ logExposure(name) {
1681
+ this.engine.logExposure(name);
1682
+ }
1665
1683
  };
1666
1684
  function dispatchSee(problem, consequence, extras, kind) {
1667
1685
  if (!_client) {
@@ -1610,6 +1610,24 @@ var Client = class {
1610
1610
  getKillswitch(name, switchKey) {
1611
1611
  return this.engine.getKillswitch(name, switchKey);
1612
1612
  }
1613
+ /**
1614
+ * Record a conversion/metric event for the bound (identified) user. Delegates
1615
+ * to {@link Engine.track} — so an experiment is end-to-end Client-only (no need
1616
+ * to drop down to the Engine to log a conversion). Fire-and-forget; no-op in
1617
+ * test mode.
1618
+ */
1619
+ track(eventName, props) {
1620
+ this.engine.track(eventName, props);
1621
+ }
1622
+ /**
1623
+ * Log an exposure for `name` at the treatment's render for the bound user.
1624
+ * Delegates to {@link Engine.logExposure} (no-op when the visitor isn't
1625
+ * enrolled). Pair with `getExperiment(name, …, { logExposure: false })` /
1626
+ * `disableAutoExposure` to log exposure exactly when you render.
1627
+ */
1628
+ logExposure(name) {
1629
+ this.engine.logExposure(name);
1630
+ }
1613
1631
  };
1614
1632
  function dispatchSee(problem, consequence, extras, kind) {
1615
1633
  if (!_client) {
@@ -318,12 +318,25 @@ declare class Engine {
318
318
  * synchronous (the methods are `async` only to satisfy the server contract).
319
319
  */
320
320
  declare class ShipeasyProvider implements Provider {
321
- private readonly client;
322
321
  readonly metadata: {
323
322
  readonly name: "shipeasy";
324
323
  };
325
324
  readonly runsOn: "server";
326
- constructor(client: Engine);
325
+ private readonly client;
326
+ /**
327
+ * Construct the provider. The **global form** (no argument) resolves the
328
+ * engine built by `configure({ apiKey })`, so the docs build it after
329
+ * configuration without ever naming the `Engine`:
330
+ *
331
+ * ```ts
332
+ * configure({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
333
+ * await OpenFeature.setProviderAndWait(new ShipeasyProvider());
334
+ * ```
335
+ *
336
+ * Passing an explicit `Engine` stays supported for advanced/back-compat use.
337
+ * Throws if no engine is passed and `configure()` has not run.
338
+ */
339
+ constructor(client?: Engine);
327
340
  /** Fetch the rules blob once. The SDK fires `Ready` when this resolves. */
328
341
  initialize(): Promise<void>;
329
342
  onClose(): Promise<void>;
@@ -318,12 +318,25 @@ declare class Engine {
318
318
  * synchronous (the methods are `async` only to satisfy the server contract).
319
319
  */
320
320
  declare class ShipeasyProvider implements Provider {
321
- private readonly client;
322
321
  readonly metadata: {
323
322
  readonly name: "shipeasy";
324
323
  };
325
324
  readonly runsOn: "server";
326
- constructor(client: Engine);
325
+ private readonly client;
326
+ /**
327
+ * Construct the provider. The **global form** (no argument) resolves the
328
+ * engine built by `configure({ apiKey })`, so the docs build it after
329
+ * configuration without ever naming the `Engine`:
330
+ *
331
+ * ```ts
332
+ * configure({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
333
+ * await OpenFeature.setProviderAndWait(new ShipeasyProvider());
334
+ * ```
335
+ *
336
+ * Passing an explicit `Engine` stays supported for advanced/back-compat use.
337
+ * Throws if no engine is passed and `configure()` has not run.
338
+ */
339
+ constructor(client?: Engine);
327
340
  /** Fetch the rules blob once. The SDK fires `Ready` when this resolves. */
328
341
  initialize(): Promise<void>;
329
342
  onClose(): Promise<void>;