@shipeasy/sdk 5.3.0 → 6.0.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
@@ -17,42 +17,121 @@ pnpm add @shipeasy/sdk
17
17
  For React projects use [`@shipeasy/sdk-react`](https://github.com/shipeasy-ai/sdk-react)
18
18
  which wraps this package with a `<ShipeasyProvider>` and hooks.
19
19
 
20
- ## Quickstart — browser
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
21
29
 
22
30
  ```ts
23
- import { FlagsClientBrowser } from "@shipeasy/sdk/client";
31
+ import { configure, Client } from "@shipeasy/sdk/client";
24
32
 
25
- const client = new FlagsClientBrowser({
26
- sdkKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY!,
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 }),
27
38
  });
28
39
 
29
- await client.identify({ user_id: "user-123", plan: "pro" });
40
+ // Per visitor:
41
+ const flags = new Client(currentUser);
42
+ await flags.ready(); // optional — await the first /sdk/evaluate round-trip
30
43
 
31
- if (client.getFlag("new_checkout")) {
44
+ if (flags.getFlag("new_checkout")) {
32
45
  // ship it
33
46
  }
34
47
 
35
- const cfg = client.getConfig<{ max_uploads: number }>("upload_limits");
36
- const { params } = client.getExperiment("hero_cta", {
37
- primary_label: "Sign up",
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 }),
38
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");
39
73
  ```
40
74
 
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.
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 }`).
45
78
 
46
- ## Quickstart server (Node, Cloudflare Worker, Deno)
79
+ ### Low-level: the `Engine` directly
80
+
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:
47
85
 
48
86
  ```ts
49
- import { FlagsClient } from "@shipeasy/sdk/server";
87
+ import { Engine } from "@shipeasy/sdk/server";
50
88
 
51
- const client = new FlagsClient({ sdkKey: process.env.SHIPEASY_SERVER_KEY! });
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
+ ```
52
93
 
53
- const cfg = await client.getConfig("plan_limits", { user_id: "u-1" });
94
+ ## SSR bootstrap (flags on first paint)
95
+
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).
101
+
102
+ ```tsx
103
+ // app/layout.tsx — Next.js root layout (React Server Component)
104
+ import { shipeasy } from "@shipeasy/sdk/server";
105
+
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
+ }
54
124
  ```
55
125
 
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.
131
+
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
+
56
135
  ## Error tracking — `see()`
57
136
 
58
137
  `see` (shipeasy error) is the structured error reporter — opes-style: every
@@ -140,9 +219,9 @@ so your tests never touch the network.
140
219
 
141
220
  ```ts
142
221
  // Server (Node / Cloudflare Worker / Deno)
143
- import { FlagsClient } from "@shipeasy/sdk/server";
222
+ import { Engine } from "@shipeasy/sdk/server";
144
223
 
145
- const client = FlagsClient.forTesting();
224
+ const client = Engine.forTesting();
146
225
 
147
226
  client.overrideFlag("new_checkout", true);
148
227
  client.overrideConfig("upload_limits", { max_uploads: 50 });
@@ -159,9 +238,9 @@ client.clearOverrides(); // reset every override back to default
159
238
 
160
239
  ```ts
161
240
  // Browser (vanilla JS — no React required)
162
- import { FlagsClientBrowser } from "@shipeasy/sdk/client";
241
+ import { Engine } from "@shipeasy/sdk/client";
163
242
 
164
- const client = FlagsClientBrowser.forTesting();
243
+ const client = Engine.forTesting();
165
244
 
166
245
  client.overrideFlag("new_checkout", true);
167
246
  client.overrideConfig("upload_limits", { max_uploads: 50 });
@@ -242,13 +321,13 @@ enabled/killed state into a boolean, so `OFF` folds into `DEFAULT` there.
242
321
 
243
322
  ## Change listeners
244
323
 
245
- The server `FlagsClient` fires registered listeners after a **background poll
324
+ The server `Engine` fires registered listeners after a **background poll
246
325
  returns new data** (HTTP 200, not 304) — handy for invalidating a cache or
247
326
  re-rendering when a flag flips. Returns an unsubscribe function. Never fires in
248
327
  test/offline mode (no polling happens):
249
328
 
250
329
  ```ts
251
- const client = new FlagsClient({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
330
+ const client = new Engine({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
252
331
  await client.init();
253
332
  const unsubscribe = client.onChange(() => {
254
333
  console.log("flag rules changed — re-evaluating");
@@ -257,7 +336,7 @@ const unsubscribe = client.onChange(() => {
257
336
  unsubscribe();
258
337
  ```
259
338
 
260
- The browser `FlagsClientBrowser` already exposes the equivalent `subscribe()`
339
+ The browser `Engine` already exposes the equivalent `subscribe()`
261
340
  (fires after each `identify()` / override change).
262
341
 
263
342
  ## Offline snapshot
@@ -276,11 +355,11 @@ two SDK wire bodies:
276
355
  ```
277
356
 
278
357
  ```ts
279
- import { FlagsClient } from "@shipeasy/sdk/server";
358
+ import { Engine } from "@shipeasy/sdk/server";
280
359
 
281
- const client = FlagsClient.fromFile("./snapshot.json");
360
+ const client = Engine.fromFile("./snapshot.json");
282
361
  // or, if you already hold the parsed object:
283
- const client = FlagsClient.fromSnapshot({ flags, experiments });
362
+ const client = Engine.fromSnapshot({ flags, experiments });
284
363
 
285
364
  client.getFlag("new_checkout", { user_id: "u1" });
286
365
  ```
@@ -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.
@@ -530,8 +530,8 @@ interface BootstrapPayload {
530
530
  /** i18n profile the server rendered with, so the client loader matches. No key is embedded. */
531
531
  i18nProfile?: string;
532
532
  apiUrl?: string;
533
- /** When true, tEl() returns marker-wrapped strings for devtools label editing. */
534
- editLabels?: boolean;
533
+ /** Stable anonymous bucketing id the server evaluated against (cross-SDK contract). */
534
+ anonId?: string;
535
535
  }
536
536
  /**
537
537
  * Universal flags facade. Methods return safe defaults when the singleton
@@ -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,78 @@ 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
+ }
586
658
  interface SeeApi {
587
659
  /**
588
660
  * Report a handled problem and its product consequence:
@@ -674,4 +746,4 @@ interface I18nFacade {
674
746
  }
675
747
  declare const i18n: I18nFacade;
676
748
 
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 };
749
+ export { type AttributesFn, type AutoCollectGroups, type BootstrapPayload, Client, type ConfigureOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, FLAG_REASONS, type FlagDetail, type FlagReason, type GetConfigOptions, type GetExperimentOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetConfigureForTests, _resetShipeasyForTests, attachDevtools, configure, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
@@ -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.
@@ -530,8 +530,8 @@ interface BootstrapPayload {
530
530
  /** i18n profile the server rendered with, so the client loader matches. No key is embedded. */
531
531
  i18nProfile?: string;
532
532
  apiUrl?: string;
533
- /** When true, tEl() returns marker-wrapped strings for devtools label editing. */
534
- editLabels?: boolean;
533
+ /** Stable anonymous bucketing id the server evaluated against (cross-SDK contract). */
534
+ anonId?: string;
535
535
  }
536
536
  /**
537
537
  * Universal flags facade. Methods return safe defaults when the singleton
@@ -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,78 @@ 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
+ }
586
658
  interface SeeApi {
587
659
  /**
588
660
  * Report a handled problem and its product consequence:
@@ -674,4 +746,4 @@ interface I18nFacade {
674
746
  }
675
747
  declare const i18n: I18nFacade;
676
748
 
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 };
749
+ export { type AttributesFn, type AutoCollectGroups, type BootstrapPayload, Client, type ConfigureOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, FLAG_REASONS, type FlagDetail, type FlagReason, type GetConfigOptions, type GetExperimentOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetConfigureForTests, _resetShipeasyForTests, attachDevtools, configure, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };