@shipeasy/sdk 6.3.1 → 7.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
@@ -83,7 +83,7 @@ Copy-paste snippets live under [`docs/snippets/`](https://github.com/shipeasy-ai
83
83
 
84
84
  ## Testing
85
85
 
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.
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()` is a no-op, `assign()` logs no exposure, and telemetry is off — your tests never touch the network.
87
87
 
88
88
  ```ts
89
89
  import { configureForTesting, Client, clearOverrides } from "@shipeasy/sdk/server"; // or /client
@@ -92,14 +92,11 @@ import { configureForTesting, Client, clearOverrides } from "@shipeasy/sdk/serve
92
92
  configureForTesting({
93
93
  flags: { new_checkout: true },
94
94
  configs: { upload_limits: { max_uploads: 50 } },
95
- experiments: { hero_cta: ["treatment", { primary_label: "Buy now" }] },
96
95
  });
97
96
 
98
97
  const flags = new Client({ user_id: "u_1" }); // construct once per callsite
99
98
  flags.getFlag("new_checkout"); // true
100
99
  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" } }
103
100
 
104
101
  clearOverrides(); // reset every seeded override back to the empty-blob default
105
102
  ```
@@ -169,11 +169,19 @@ interface EvalExpResult {
169
169
  inExperiment: boolean;
170
170
  group: string;
171
171
  params: Record<string, unknown>;
172
+ /** The universe this experiment belongs to — lets `universe(name).assign()`
173
+ * find the enrolled experiment within a universe. */
174
+ universe?: string;
172
175
  }
173
176
  interface EvalResponse {
174
177
  flags: Record<string, boolean>;
175
178
  configs: Record<string, unknown>;
176
179
  experiments: Record<string, EvalExpResult>;
180
+ /** Per-universe param defaults (name → default map) so `universe(name).get()`
181
+ * resolves to the universe default even when the unit is not enrolled. */
182
+ universes?: Record<string, {
183
+ defaults: Record<string, unknown>;
184
+ }>;
177
185
  /**
178
186
  * Killswitch state, flattened by the server. A boolean means the killswitch
179
187
  * is whole-killed; an object means it's not whole-killed and carries per-
@@ -187,6 +195,23 @@ interface EvalResponse {
187
195
  */
188
196
  sticky?: Record<string, StickyCookieEntry>;
189
197
  }
198
+ /**
199
+ * The result of `universe(name).assign()` — the visitor's standing in a universe.
200
+ * A universe is a mutual-exclusion pool, so the visitor lands in **at most one**
201
+ * experiment. Never throws: an un-enrolled visitor still resolves `get()` to the
202
+ * universe defaults (or your fallback). `assign()` auto-logs one exposure when
203
+ * enrolled (subject to `disableAutoExposure`).
204
+ */
205
+ interface Assignment {
206
+ /** The experiment the visitor landed in, or `null` when not enrolled. */
207
+ readonly name: string | null;
208
+ /** The assigned variant/group name, or `null` when not enrolled. */
209
+ readonly group: string | null;
210
+ /** True iff the visitor is enrolled in an experiment in this universe. */
211
+ readonly enrolled: boolean;
212
+ /** Read a resolved param: variant override ?? universe default ?? `fallback`. */
213
+ get<T = unknown>(field: string, fallback?: T): T | undefined;
214
+ }
190
215
  /** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
191
216
  interface StickyCookieEntry {
192
217
  g: string;
@@ -207,6 +232,33 @@ declare function sameOrigin(rawUrl: string): boolean;
207
232
  */
208
233
  declare function injectCorrelationHeader(args: Parameters<typeof fetch>, corr: string): Parameters<typeof fetch>;
209
234
  type EngineEnv = "dev" | "staging" | "prod";
235
+ /**
236
+ * A pluggable persistence backend for the anonymous id. Primarily for non-DOM
237
+ * runtimes (React Native / Expo) where there is no cookie or `localStorage`, so
238
+ * the anon id would otherwise regenerate every app launch and re-bucket the
239
+ * visitor. Point it at `@react-native-async-storage/async-storage` (or any
240
+ * store) and the SDK hydrates a stable id before the first `/sdk/evaluate`:
241
+ *
242
+ * ```ts
243
+ * import AsyncStorage from "@react-native-async-storage/async-storage";
244
+ * configure({
245
+ * clientKey: "...",
246
+ * anonymousStore: {
247
+ * get: (k) => AsyncStorage.getItem(k),
248
+ * set: (k, v) => AsyncStorage.setItem(k, v),
249
+ * remove: (k) => AsyncStorage.removeItem(k),
250
+ * },
251
+ * });
252
+ * ```
253
+ *
254
+ * Methods may be sync or async — the SDK awaits either. Any thrown error is
255
+ * swallowed (the in-memory id is used as a fallback).
256
+ */
257
+ interface AnonymousStore {
258
+ get(key: string): string | null | Promise<string | null>;
259
+ set(key: string, value: string): void | Promise<void>;
260
+ remove(key: string): void | Promise<void>;
261
+ }
210
262
  interface EngineOptions {
211
263
  sdkKey: string;
212
264
  baseUrl?: string;
@@ -244,6 +296,14 @@ interface EngineOptions {
244
296
  * `silent` < `error` < `warn` < `info` < `debug`. Defaults to `"warn"`.
245
297
  */
246
298
  logLevel?: LogLevel;
299
+ /**
300
+ * Opt out of SDK-internal error self-monitoring. When one of the SDK's own
301
+ * last-resort guards catches an internal failure (an "on our end" bug), the
302
+ * SDK reports it to Shipeasy's own project so we can track SDK bugs across
303
+ * apps — never to your project. ON by default; forced off in test mode. Pass
304
+ * `true` to disable.
305
+ */
306
+ disableInternalErrorReporting?: boolean;
247
307
  /**
248
308
  * Suppress automatic exposure logging in `getExperiment` (Statsig's
249
309
  * `disableExposureLogging`). Default false — reading an enrolled variant
@@ -267,6 +327,14 @@ interface EngineOptions {
267
327
  * lever. Pass `false` to opt out (pure deterministic eval).
268
328
  */
269
329
  stickyBucketing?: boolean;
330
+ /**
331
+ * Pluggable persistence for the anonymous id (React Native / Expo, or any
332
+ * runtime without a cookie / `localStorage`). When set, the SDK hydrates the
333
+ * stored id before the first `/sdk/evaluate` so bucketing stays stable across
334
+ * app launches; on a fresh device it persists the freshly-minted id. See
335
+ * {@link AnonymousStore}.
336
+ */
337
+ anonymousStore?: AnonymousStore;
270
338
  /**
271
339
  * Test mode — no network at all. identify()/init are no-ops (never call
272
340
  * /sdk/evaluate), track() is a no-op, telemetry is forced off, and the client
@@ -287,6 +355,7 @@ declare class Engine {
287
355
  private readonly env;
288
356
  private evalResult;
289
357
  private anonId;
358
+ private anonReady;
290
359
  private userId;
291
360
  private buffer;
292
361
  private telemetry;
@@ -301,6 +370,13 @@ declare class Engine {
301
370
  private readonly experimentOverrides;
302
371
  private onOverrideChange;
303
372
  constructor(opts: EngineOptions);
373
+ /**
374
+ * Consult the caller-supplied {@link AnonymousStore}: adopt a persisted id so
375
+ * the visitor buckets identically across app launches, or persist the id just
376
+ * minted by getOrCreateAnonId() on a fresh device. Best-effort — any store
377
+ * failure leaves the in-memory id in place.
378
+ */
379
+ private hydrateAnonFromStore;
304
380
  /**
305
381
  * Build a no-network, immediately-usable browser client for tests
306
382
  * (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
@@ -354,16 +430,21 @@ declare class Engine {
354
430
  getFlag(name: string, defaultValue?: boolean): boolean;
355
431
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
356
432
  getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
357
- getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
358
- getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, opts: GetExperimentOptions<P>): ExperimentResult<P>;
359
433
  /**
360
- * Manually log an exposure for an enrolled experiment (Statsig's
361
- * `manuallyLogExposure`). Reads the cached eval result; if the visitor is in
362
- * the experiment, pushes the session-deduped exposure. Pair this with the
363
- * render of the treatment when reading with `{ logExposure: false }` (or
364
- * `disableAutoExposure: true`). No-op if the visitor isn't enrolled.
365
- */
366
- logExposure(name: string): void;
434
+ * Assign the visitor within a universe: `universe("checkout").assign()`. A
435
+ * universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
436
+ * the returned {@link Assignment} exposes `.group` / `.get(field, fallback)`
437
+ * and auto-logs one exposure when enrolled (unless `disableAutoExposure` or
438
+ * `assign({ logExposure: false })`). An un-enrolled visitor still resolves
439
+ * `get()` to the universe defaults. This is the sole experiment read path —
440
+ * there is no `getExperiment` (ask a universe, not an experiment).
441
+ */
442
+ universe(name: string): {
443
+ assign(opts?: {
444
+ logExposure?: boolean;
445
+ }): Assignment;
446
+ };
447
+ private assignUniverse;
367
448
  /**
368
449
  * Subscribe to state changes — fires after identify() completes and on
369
450
  * `se:override:change` events from the devtools overlay. Returns an
@@ -493,6 +574,13 @@ interface ShipeasyClientConfig {
493
574
  * counted by Cloudflare's native per-path analytics. Pass `true` to opt out.
494
575
  */
495
576
  disableTelemetry?: boolean;
577
+ /**
578
+ * Opt out of SDK-internal error self-monitoring. Internal SDK failures ("on
579
+ * our end") are reported to Shipeasy's own project (never yours) so we can
580
+ * track SDK bugs. ON by default. See
581
+ * {@link EngineOptions.disableInternalErrorReporting}.
582
+ */
583
+ disableInternalErrorReporting?: boolean;
496
584
  /**
497
585
  * Suppress automatic exposure logging in `flags.getExperiment` (Statsig's
498
586
  * `disableExposureLogging`). Default false. When true, call
@@ -512,6 +600,12 @@ interface ShipeasyClientConfig {
512
600
  * opt out. See {@link EngineOptions.stickyBucketing}.
513
601
  */
514
602
  stickyBucketing?: boolean;
603
+ /**
604
+ * Pluggable anon-id persistence for non-DOM runtimes (React Native / Expo).
605
+ * Back it with AsyncStorage so the anonymous id — and thus bucketing — stays
606
+ * stable across app launches. See {@link AnonymousStore}.
607
+ */
608
+ anonymousStore?: AnonymousStore;
515
609
  }
516
610
  /**
517
611
  * Initialise the ShipEasy client SDK and wire up lazy devtools.
@@ -571,10 +665,19 @@ declare const flags: {
571
665
  /** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
572
666
  getDetail(name: string): FlagDetail;
573
667
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
574
- getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decodeOrOpts?: ((raw: unknown) => P) | GetExperimentOptions<P>, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
575
- /** Manually log an exposure for an enrolled experiment. See
576
- * {@link Engine.logExposure}. No-op before configure(). */
577
- logExposure(name: string): void;
668
+ /**
669
+ * Assign the visitor within a universe: `flags.universe("checkout").assign()`.
670
+ * A universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
671
+ * the returned {@link Assignment} exposes `.group` / `.get(field, fallback)`
672
+ * and auto-logs one exposure when enrolled. Before configure() (or on error)
673
+ * returns a safe not-enrolled handle. Replaces the removed `getExperiment` —
674
+ * read experiments by universe, never by name.
675
+ */
676
+ universe(name: string): {
677
+ assign(opts?: {
678
+ logExposure?: boolean;
679
+ }): Assignment;
680
+ };
578
681
  track(eventName: string, props?: Record<string, unknown>): void;
579
682
  /**
580
683
  * Read a killswitch. Without `switchKey`, returns true when the killswitch is
@@ -670,7 +773,17 @@ declare class Client<U = unknown> {
670
773
  getFlagDetail(name: string): FlagDetail;
671
774
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
672
775
  getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
673
- getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
776
+ /**
777
+ * Assign the visitor within a universe: `client.universe("checkout").assign()`.
778
+ * A universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
779
+ * returns an {@link Assignment} (`.group` / `.get(field, fallback)`) and
780
+ * auto-logs one exposure when enrolled. Replaces the removed `getExperiment`.
781
+ */
782
+ universe(name: string): {
783
+ assign(opts?: {
784
+ logExposure?: boolean;
785
+ }): Assignment;
786
+ };
674
787
  /** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
675
788
  getKillswitch(name: string, switchKey?: string): boolean;
676
789
  /**
@@ -680,13 +793,6 @@ declare class Client<U = unknown> {
680
793
  * test mode.
681
794
  */
682
795
  track(eventName: string, props?: Record<string, unknown>): void;
683
- /**
684
- * Log an exposure for `name` at the treatment's render for the bound user.
685
- * Delegates to {@link Engine.logExposure} (no-op when the visitor isn't
686
- * enrolled). Pair with `getExperiment(name, …, { logExposure: false })` /
687
- * `disableAutoExposure` to log exposure exactly when you render.
688
- */
689
- logExposure(name: string): void;
690
796
  }
691
797
  interface SeeApi {
692
798
  /**
@@ -779,4 +885,4 @@ interface I18nFacade {
779
885
  }
780
886
  declare const i18n: I18nFacade;
781
887
 
782
- export { type AttributesFn, type AutoCollectGroups, type BootstrapPayload, Client, type ConfigureOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, FLAG_REASONS, type FlagDetail, type FlagReason, type GetConfigOptions, type GetExperimentOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, LOG_LEVELS, type LabelAttrs, type LogLevel, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetConfigureForTests, _resetShipeasyForTests, attachDevtools, configure, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
888
+ export { type AnonymousStore, type Assignment, type AttributesFn, type AutoCollectGroups, type BootstrapPayload, Client, type ConfigureOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, FLAG_REASONS, type FlagDetail, type FlagReason, type GetConfigOptions, type GetExperimentOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, LOG_LEVELS, type LabelAttrs, type LogLevel, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetConfigureForTests, _resetShipeasyForTests, attachDevtools, configure, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
@@ -169,11 +169,19 @@ interface EvalExpResult {
169
169
  inExperiment: boolean;
170
170
  group: string;
171
171
  params: Record<string, unknown>;
172
+ /** The universe this experiment belongs to — lets `universe(name).assign()`
173
+ * find the enrolled experiment within a universe. */
174
+ universe?: string;
172
175
  }
173
176
  interface EvalResponse {
174
177
  flags: Record<string, boolean>;
175
178
  configs: Record<string, unknown>;
176
179
  experiments: Record<string, EvalExpResult>;
180
+ /** Per-universe param defaults (name → default map) so `universe(name).get()`
181
+ * resolves to the universe default even when the unit is not enrolled. */
182
+ universes?: Record<string, {
183
+ defaults: Record<string, unknown>;
184
+ }>;
177
185
  /**
178
186
  * Killswitch state, flattened by the server. A boolean means the killswitch
179
187
  * is whole-killed; an object means it's not whole-killed and carries per-
@@ -187,6 +195,23 @@ interface EvalResponse {
187
195
  */
188
196
  sticky?: Record<string, StickyCookieEntry>;
189
197
  }
198
+ /**
199
+ * The result of `universe(name).assign()` — the visitor's standing in a universe.
200
+ * A universe is a mutual-exclusion pool, so the visitor lands in **at most one**
201
+ * experiment. Never throws: an un-enrolled visitor still resolves `get()` to the
202
+ * universe defaults (or your fallback). `assign()` auto-logs one exposure when
203
+ * enrolled (subject to `disableAutoExposure`).
204
+ */
205
+ interface Assignment {
206
+ /** The experiment the visitor landed in, or `null` when not enrolled. */
207
+ readonly name: string | null;
208
+ /** The assigned variant/group name, or `null` when not enrolled. */
209
+ readonly group: string | null;
210
+ /** True iff the visitor is enrolled in an experiment in this universe. */
211
+ readonly enrolled: boolean;
212
+ /** Read a resolved param: variant override ?? universe default ?? `fallback`. */
213
+ get<T = unknown>(field: string, fallback?: T): T | undefined;
214
+ }
190
215
  /** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
191
216
  interface StickyCookieEntry {
192
217
  g: string;
@@ -207,6 +232,33 @@ declare function sameOrigin(rawUrl: string): boolean;
207
232
  */
208
233
  declare function injectCorrelationHeader(args: Parameters<typeof fetch>, corr: string): Parameters<typeof fetch>;
209
234
  type EngineEnv = "dev" | "staging" | "prod";
235
+ /**
236
+ * A pluggable persistence backend for the anonymous id. Primarily for non-DOM
237
+ * runtimes (React Native / Expo) where there is no cookie or `localStorage`, so
238
+ * the anon id would otherwise regenerate every app launch and re-bucket the
239
+ * visitor. Point it at `@react-native-async-storage/async-storage` (or any
240
+ * store) and the SDK hydrates a stable id before the first `/sdk/evaluate`:
241
+ *
242
+ * ```ts
243
+ * import AsyncStorage from "@react-native-async-storage/async-storage";
244
+ * configure({
245
+ * clientKey: "...",
246
+ * anonymousStore: {
247
+ * get: (k) => AsyncStorage.getItem(k),
248
+ * set: (k, v) => AsyncStorage.setItem(k, v),
249
+ * remove: (k) => AsyncStorage.removeItem(k),
250
+ * },
251
+ * });
252
+ * ```
253
+ *
254
+ * Methods may be sync or async — the SDK awaits either. Any thrown error is
255
+ * swallowed (the in-memory id is used as a fallback).
256
+ */
257
+ interface AnonymousStore {
258
+ get(key: string): string | null | Promise<string | null>;
259
+ set(key: string, value: string): void | Promise<void>;
260
+ remove(key: string): void | Promise<void>;
261
+ }
210
262
  interface EngineOptions {
211
263
  sdkKey: string;
212
264
  baseUrl?: string;
@@ -244,6 +296,14 @@ interface EngineOptions {
244
296
  * `silent` < `error` < `warn` < `info` < `debug`. Defaults to `"warn"`.
245
297
  */
246
298
  logLevel?: LogLevel;
299
+ /**
300
+ * Opt out of SDK-internal error self-monitoring. When one of the SDK's own
301
+ * last-resort guards catches an internal failure (an "on our end" bug), the
302
+ * SDK reports it to Shipeasy's own project so we can track SDK bugs across
303
+ * apps — never to your project. ON by default; forced off in test mode. Pass
304
+ * `true` to disable.
305
+ */
306
+ disableInternalErrorReporting?: boolean;
247
307
  /**
248
308
  * Suppress automatic exposure logging in `getExperiment` (Statsig's
249
309
  * `disableExposureLogging`). Default false — reading an enrolled variant
@@ -267,6 +327,14 @@ interface EngineOptions {
267
327
  * lever. Pass `false` to opt out (pure deterministic eval).
268
328
  */
269
329
  stickyBucketing?: boolean;
330
+ /**
331
+ * Pluggable persistence for the anonymous id (React Native / Expo, or any
332
+ * runtime without a cookie / `localStorage`). When set, the SDK hydrates the
333
+ * stored id before the first `/sdk/evaluate` so bucketing stays stable across
334
+ * app launches; on a fresh device it persists the freshly-minted id. See
335
+ * {@link AnonymousStore}.
336
+ */
337
+ anonymousStore?: AnonymousStore;
270
338
  /**
271
339
  * Test mode — no network at all. identify()/init are no-ops (never call
272
340
  * /sdk/evaluate), track() is a no-op, telemetry is forced off, and the client
@@ -287,6 +355,7 @@ declare class Engine {
287
355
  private readonly env;
288
356
  private evalResult;
289
357
  private anonId;
358
+ private anonReady;
290
359
  private userId;
291
360
  private buffer;
292
361
  private telemetry;
@@ -301,6 +370,13 @@ declare class Engine {
301
370
  private readonly experimentOverrides;
302
371
  private onOverrideChange;
303
372
  constructor(opts: EngineOptions);
373
+ /**
374
+ * Consult the caller-supplied {@link AnonymousStore}: adopt a persisted id so
375
+ * the visitor buckets identically across app launches, or persist the id just
376
+ * minted by getOrCreateAnonId() on a fresh device. Best-effort — any store
377
+ * failure leaves the in-memory id in place.
378
+ */
379
+ private hydrateAnonFromStore;
304
380
  /**
305
381
  * Build a no-network, immediately-usable browser client for tests
306
382
  * (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
@@ -354,16 +430,21 @@ declare class Engine {
354
430
  getFlag(name: string, defaultValue?: boolean): boolean;
355
431
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
356
432
  getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
357
- getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
358
- getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, opts: GetExperimentOptions<P>): ExperimentResult<P>;
359
433
  /**
360
- * Manually log an exposure for an enrolled experiment (Statsig's
361
- * `manuallyLogExposure`). Reads the cached eval result; if the visitor is in
362
- * the experiment, pushes the session-deduped exposure. Pair this with the
363
- * render of the treatment when reading with `{ logExposure: false }` (or
364
- * `disableAutoExposure: true`). No-op if the visitor isn't enrolled.
365
- */
366
- logExposure(name: string): void;
434
+ * Assign the visitor within a universe: `universe("checkout").assign()`. A
435
+ * universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
436
+ * the returned {@link Assignment} exposes `.group` / `.get(field, fallback)`
437
+ * and auto-logs one exposure when enrolled (unless `disableAutoExposure` or
438
+ * `assign({ logExposure: false })`). An un-enrolled visitor still resolves
439
+ * `get()` to the universe defaults. This is the sole experiment read path —
440
+ * there is no `getExperiment` (ask a universe, not an experiment).
441
+ */
442
+ universe(name: string): {
443
+ assign(opts?: {
444
+ logExposure?: boolean;
445
+ }): Assignment;
446
+ };
447
+ private assignUniverse;
367
448
  /**
368
449
  * Subscribe to state changes — fires after identify() completes and on
369
450
  * `se:override:change` events from the devtools overlay. Returns an
@@ -493,6 +574,13 @@ interface ShipeasyClientConfig {
493
574
  * counted by Cloudflare's native per-path analytics. Pass `true` to opt out.
494
575
  */
495
576
  disableTelemetry?: boolean;
577
+ /**
578
+ * Opt out of SDK-internal error self-monitoring. Internal SDK failures ("on
579
+ * our end") are reported to Shipeasy's own project (never yours) so we can
580
+ * track SDK bugs. ON by default. See
581
+ * {@link EngineOptions.disableInternalErrorReporting}.
582
+ */
583
+ disableInternalErrorReporting?: boolean;
496
584
  /**
497
585
  * Suppress automatic exposure logging in `flags.getExperiment` (Statsig's
498
586
  * `disableExposureLogging`). Default false. When true, call
@@ -512,6 +600,12 @@ interface ShipeasyClientConfig {
512
600
  * opt out. See {@link EngineOptions.stickyBucketing}.
513
601
  */
514
602
  stickyBucketing?: boolean;
603
+ /**
604
+ * Pluggable anon-id persistence for non-DOM runtimes (React Native / Expo).
605
+ * Back it with AsyncStorage so the anonymous id — and thus bucketing — stays
606
+ * stable across app launches. See {@link AnonymousStore}.
607
+ */
608
+ anonymousStore?: AnonymousStore;
515
609
  }
516
610
  /**
517
611
  * Initialise the ShipEasy client SDK and wire up lazy devtools.
@@ -571,10 +665,19 @@ declare const flags: {
571
665
  /** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
572
666
  getDetail(name: string): FlagDetail;
573
667
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
574
- getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decodeOrOpts?: ((raw: unknown) => P) | GetExperimentOptions<P>, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
575
- /** Manually log an exposure for an enrolled experiment. See
576
- * {@link Engine.logExposure}. No-op before configure(). */
577
- logExposure(name: string): void;
668
+ /**
669
+ * Assign the visitor within a universe: `flags.universe("checkout").assign()`.
670
+ * A universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
671
+ * the returned {@link Assignment} exposes `.group` / `.get(field, fallback)`
672
+ * and auto-logs one exposure when enrolled. Before configure() (or on error)
673
+ * returns a safe not-enrolled handle. Replaces the removed `getExperiment` —
674
+ * read experiments by universe, never by name.
675
+ */
676
+ universe(name: string): {
677
+ assign(opts?: {
678
+ logExposure?: boolean;
679
+ }): Assignment;
680
+ };
578
681
  track(eventName: string, props?: Record<string, unknown>): void;
579
682
  /**
580
683
  * Read a killswitch. Without `switchKey`, returns true when the killswitch is
@@ -670,7 +773,17 @@ declare class Client<U = unknown> {
670
773
  getFlagDetail(name: string): FlagDetail;
671
774
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
672
775
  getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
673
- getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
776
+ /**
777
+ * Assign the visitor within a universe: `client.universe("checkout").assign()`.
778
+ * A universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
779
+ * returns an {@link Assignment} (`.group` / `.get(field, fallback)`) and
780
+ * auto-logs one exposure when enrolled. Replaces the removed `getExperiment`.
781
+ */
782
+ universe(name: string): {
783
+ assign(opts?: {
784
+ logExposure?: boolean;
785
+ }): Assignment;
786
+ };
674
787
  /** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
675
788
  getKillswitch(name: string, switchKey?: string): boolean;
676
789
  /**
@@ -680,13 +793,6 @@ declare class Client<U = unknown> {
680
793
  * test mode.
681
794
  */
682
795
  track(eventName: string, props?: Record<string, unknown>): void;
683
- /**
684
- * Log an exposure for `name` at the treatment's render for the bound user.
685
- * Delegates to {@link Engine.logExposure} (no-op when the visitor isn't
686
- * enrolled). Pair with `getExperiment(name, …, { logExposure: false })` /
687
- * `disableAutoExposure` to log exposure exactly when you render.
688
- */
689
- logExposure(name: string): void;
690
796
  }
691
797
  interface SeeApi {
692
798
  /**
@@ -779,4 +885,4 @@ interface I18nFacade {
779
885
  }
780
886
  declare const i18n: I18nFacade;
781
887
 
782
- export { type AttributesFn, type AutoCollectGroups, type BootstrapPayload, Client, type ConfigureOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, FLAG_REASONS, type FlagDetail, type FlagReason, type GetConfigOptions, type GetExperimentOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, LOG_LEVELS, type LabelAttrs, type LogLevel, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetConfigureForTests, _resetShipeasyForTests, attachDevtools, configure, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
888
+ export { type AnonymousStore, type Assignment, type AttributesFn, type AutoCollectGroups, type BootstrapPayload, Client, type ConfigureOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, FLAG_REASONS, type FlagDetail, type FlagReason, type GetConfigOptions, type GetExperimentOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, LOG_LEVELS, type LabelAttrs, type LogLevel, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetConfigureForTests, _resetShipeasyForTests, attachDevtools, configure, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };