@shipeasy/sdk 6.2.0 → 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 +1 -4
- package/dist/client/index.d.mts +147 -22
- package/dist/client/index.d.ts +147 -22
- package/dist/client/index.js +304 -125
- package/dist/client/index.mjs +303 -125
- package/dist/openfeature-server/index.d.mts +129 -8
- package/dist/openfeature-server/index.d.ts +129 -8
- package/dist/openfeature-server/index.js +66 -1
- package/dist/openfeature-server/index.mjs +66 -1
- package/dist/openfeature-web/index.d.mts +99 -27
- package/dist/openfeature-web/index.d.ts +99 -27
- package/dist/server/index.d.mts +166 -21
- package/dist/server/index.d.ts +166 -21
- package/dist/server/index.js +358 -131
- package/dist/server/index.mjs +357 -131
- package/docs/skill/SKILL.md +29 -25
- package/package.json +9 -6
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()`
|
|
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
|
```
|
package/dist/client/index.d.mts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
type LogLevel = "silent" | "error" | "warn" | "info" | "debug";
|
|
2
|
+
/** All accepted {@link LogLevel} values, in increasing verbosity. */
|
|
3
|
+
declare const LOG_LEVELS: readonly LogLevel[];
|
|
4
|
+
|
|
1
5
|
type SeeExtras = Record<string, string | number | boolean | null | undefined>;
|
|
2
6
|
type SeeKind = "caught" | "uncaught" | "unhandled_rejection" | "network" | "violation";
|
|
3
7
|
/** Built by `causesThe(subject).to(outcome)` — never constructed by hand. */
|
|
@@ -165,11 +169,19 @@ interface EvalExpResult {
|
|
|
165
169
|
inExperiment: boolean;
|
|
166
170
|
group: string;
|
|
167
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;
|
|
168
175
|
}
|
|
169
176
|
interface EvalResponse {
|
|
170
177
|
flags: Record<string, boolean>;
|
|
171
178
|
configs: Record<string, unknown>;
|
|
172
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
|
+
}>;
|
|
173
185
|
/**
|
|
174
186
|
* Killswitch state, flattened by the server. A boolean means the killswitch
|
|
175
187
|
* is whole-killed; an object means it's not whole-killed and carries per-
|
|
@@ -183,6 +195,23 @@ interface EvalResponse {
|
|
|
183
195
|
*/
|
|
184
196
|
sticky?: Record<string, StickyCookieEntry>;
|
|
185
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
|
+
}
|
|
186
215
|
/** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
|
|
187
216
|
interface StickyCookieEntry {
|
|
188
217
|
g: string;
|
|
@@ -203,6 +232,33 @@ declare function sameOrigin(rawUrl: string): boolean;
|
|
|
203
232
|
*/
|
|
204
233
|
declare function injectCorrelationHeader(args: Parameters<typeof fetch>, corr: string): Parameters<typeof fetch>;
|
|
205
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
|
+
}
|
|
206
262
|
interface EngineOptions {
|
|
207
263
|
sdkKey: string;
|
|
208
264
|
baseUrl?: string;
|
|
@@ -232,6 +288,22 @@ interface EngineOptions {
|
|
|
232
288
|
disableTelemetry?: boolean;
|
|
233
289
|
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
234
290
|
telemetryUrl?: string;
|
|
291
|
+
/**
|
|
292
|
+
* How chatty the SDK is on `console` when it catches an error internally.
|
|
293
|
+
* Every public runtime method (getFlag/getConfig/getExperiment/getKillswitch/
|
|
294
|
+
* track/logExposure/see) fails silently — returning a safe default instead of
|
|
295
|
+
* throwing — and surfaces the swallowed error through this level. Ordering:
|
|
296
|
+
* `silent` < `error` < `warn` < `info` < `debug`. Defaults to `"warn"`.
|
|
297
|
+
*/
|
|
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;
|
|
235
307
|
/**
|
|
236
308
|
* Suppress automatic exposure logging in `getExperiment` (Statsig's
|
|
237
309
|
* `disableExposureLogging`). Default false — reading an enrolled variant
|
|
@@ -255,6 +327,14 @@ interface EngineOptions {
|
|
|
255
327
|
* lever. Pass `false` to opt out (pure deterministic eval).
|
|
256
328
|
*/
|
|
257
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;
|
|
258
338
|
/**
|
|
259
339
|
* Test mode — no network at all. identify()/init are no-ops (never call
|
|
260
340
|
* /sdk/evaluate), track() is a no-op, telemetry is forced off, and the client
|
|
@@ -275,6 +355,7 @@ declare class Engine {
|
|
|
275
355
|
private readonly env;
|
|
276
356
|
private evalResult;
|
|
277
357
|
private anonId;
|
|
358
|
+
private anonReady;
|
|
278
359
|
private userId;
|
|
279
360
|
private buffer;
|
|
280
361
|
private telemetry;
|
|
@@ -289,6 +370,13 @@ declare class Engine {
|
|
|
289
370
|
private readonly experimentOverrides;
|
|
290
371
|
private onOverrideChange;
|
|
291
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;
|
|
292
380
|
/**
|
|
293
381
|
* Build a no-network, immediately-usable browser client for tests
|
|
294
382
|
* (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
|
|
@@ -342,16 +430,21 @@ declare class Engine {
|
|
|
342
430
|
getFlag(name: string, defaultValue?: boolean): boolean;
|
|
343
431
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
344
432
|
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
345
|
-
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
|
|
346
|
-
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, opts: GetExperimentOptions<P>): ExperimentResult<P>;
|
|
347
433
|
/**
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
* the
|
|
351
|
-
*
|
|
352
|
-
* `
|
|
353
|
-
|
|
354
|
-
|
|
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;
|
|
355
448
|
/**
|
|
356
449
|
* Subscribe to state changes — fires after identify() completes and on
|
|
357
450
|
* `se:override:change` events from the devtools overlay. Returns an
|
|
@@ -468,12 +561,26 @@ interface ShipeasyClientConfig {
|
|
|
468
561
|
autoCollect?: boolean | (Partial<AutoCollectGroups> & {
|
|
469
562
|
always?: boolean;
|
|
470
563
|
});
|
|
564
|
+
/**
|
|
565
|
+
* How chatty the SDK is on `console` when it swallows an internal error.
|
|
566
|
+
* `silent` < `error` < `warn` < `info` < `debug`; defaults to `"warn"`. Every
|
|
567
|
+
* runtime read/track/see call fails silently to a safe default and reports the
|
|
568
|
+
* cause at this level. See {@link EngineOptions.logLevel}.
|
|
569
|
+
*/
|
|
570
|
+
logLevel?: LogLevel;
|
|
471
571
|
/**
|
|
472
572
|
* Disable per-evaluation usage telemetry. Telemetry is ON by default — every
|
|
473
573
|
* flag/config/experiment/killswitch read fires one fire-and-forget beacon
|
|
474
574
|
* counted by Cloudflare's native per-path analytics. Pass `true` to opt out.
|
|
475
575
|
*/
|
|
476
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;
|
|
477
584
|
/**
|
|
478
585
|
* Suppress automatic exposure logging in `flags.getExperiment` (Statsig's
|
|
479
586
|
* `disableExposureLogging`). Default false. When true, call
|
|
@@ -493,6 +600,12 @@ interface ShipeasyClientConfig {
|
|
|
493
600
|
* opt out. See {@link EngineOptions.stickyBucketing}.
|
|
494
601
|
*/
|
|
495
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;
|
|
496
609
|
}
|
|
497
610
|
/**
|
|
498
611
|
* Initialise the ShipEasy client SDK and wire up lazy devtools.
|
|
@@ -552,10 +665,19 @@ declare const flags: {
|
|
|
552
665
|
/** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
|
|
553
666
|
getDetail(name: string): FlagDetail;
|
|
554
667
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
*
|
|
558
|
-
|
|
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
|
+
};
|
|
559
681
|
track(eventName: string, props?: Record<string, unknown>): void;
|
|
560
682
|
/**
|
|
561
683
|
* Read a killswitch. Without `switchKey`, returns true when the killswitch is
|
|
@@ -651,7 +773,17 @@ declare class Client<U = unknown> {
|
|
|
651
773
|
getFlagDetail(name: string): FlagDetail;
|
|
652
774
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
653
775
|
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
654
|
-
|
|
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
|
+
};
|
|
655
787
|
/** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
|
|
656
788
|
getKillswitch(name: string, switchKey?: string): boolean;
|
|
657
789
|
/**
|
|
@@ -661,13 +793,6 @@ declare class Client<U = unknown> {
|
|
|
661
793
|
* test mode.
|
|
662
794
|
*/
|
|
663
795
|
track(eventName: string, props?: Record<string, unknown>): void;
|
|
664
|
-
/**
|
|
665
|
-
* Log an exposure for `name` at the treatment's render for the bound user.
|
|
666
|
-
* Delegates to {@link Engine.logExposure} (no-op when the visitor isn't
|
|
667
|
-
* enrolled). Pair with `getExperiment(name, …, { logExposure: false })` /
|
|
668
|
-
* `disableAutoExposure` to log exposure exactly when you render.
|
|
669
|
-
*/
|
|
670
|
-
logExposure(name: string): void;
|
|
671
796
|
}
|
|
672
797
|
interface SeeApi {
|
|
673
798
|
/**
|
|
@@ -760,4 +885,4 @@ interface I18nFacade {
|
|
|
760
885
|
}
|
|
761
886
|
declare const i18n: I18nFacade;
|
|
762
887
|
|
|
763
|
-
export { type AttributesFn, type AutoCollectGroups, type BootstrapPayload, Client, type ConfigureOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, FLAG_REASONS, type FlagDetail, type FlagReason, type GetConfigOptions, type GetExperimentOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetConfigureForTests, _resetShipeasyForTests, attachDevtools, configure, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
|
|
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 };
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
type LogLevel = "silent" | "error" | "warn" | "info" | "debug";
|
|
2
|
+
/** All accepted {@link LogLevel} values, in increasing verbosity. */
|
|
3
|
+
declare const LOG_LEVELS: readonly LogLevel[];
|
|
4
|
+
|
|
1
5
|
type SeeExtras = Record<string, string | number | boolean | null | undefined>;
|
|
2
6
|
type SeeKind = "caught" | "uncaught" | "unhandled_rejection" | "network" | "violation";
|
|
3
7
|
/** Built by `causesThe(subject).to(outcome)` — never constructed by hand. */
|
|
@@ -165,11 +169,19 @@ interface EvalExpResult {
|
|
|
165
169
|
inExperiment: boolean;
|
|
166
170
|
group: string;
|
|
167
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;
|
|
168
175
|
}
|
|
169
176
|
interface EvalResponse {
|
|
170
177
|
flags: Record<string, boolean>;
|
|
171
178
|
configs: Record<string, unknown>;
|
|
172
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
|
+
}>;
|
|
173
185
|
/**
|
|
174
186
|
* Killswitch state, flattened by the server. A boolean means the killswitch
|
|
175
187
|
* is whole-killed; an object means it's not whole-killed and carries per-
|
|
@@ -183,6 +195,23 @@ interface EvalResponse {
|
|
|
183
195
|
*/
|
|
184
196
|
sticky?: Record<string, StickyCookieEntry>;
|
|
185
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
|
+
}
|
|
186
215
|
/** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
|
|
187
216
|
interface StickyCookieEntry {
|
|
188
217
|
g: string;
|
|
@@ -203,6 +232,33 @@ declare function sameOrigin(rawUrl: string): boolean;
|
|
|
203
232
|
*/
|
|
204
233
|
declare function injectCorrelationHeader(args: Parameters<typeof fetch>, corr: string): Parameters<typeof fetch>;
|
|
205
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
|
+
}
|
|
206
262
|
interface EngineOptions {
|
|
207
263
|
sdkKey: string;
|
|
208
264
|
baseUrl?: string;
|
|
@@ -232,6 +288,22 @@ interface EngineOptions {
|
|
|
232
288
|
disableTelemetry?: boolean;
|
|
233
289
|
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
234
290
|
telemetryUrl?: string;
|
|
291
|
+
/**
|
|
292
|
+
* How chatty the SDK is on `console` when it catches an error internally.
|
|
293
|
+
* Every public runtime method (getFlag/getConfig/getExperiment/getKillswitch/
|
|
294
|
+
* track/logExposure/see) fails silently — returning a safe default instead of
|
|
295
|
+
* throwing — and surfaces the swallowed error through this level. Ordering:
|
|
296
|
+
* `silent` < `error` < `warn` < `info` < `debug`. Defaults to `"warn"`.
|
|
297
|
+
*/
|
|
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;
|
|
235
307
|
/**
|
|
236
308
|
* Suppress automatic exposure logging in `getExperiment` (Statsig's
|
|
237
309
|
* `disableExposureLogging`). Default false — reading an enrolled variant
|
|
@@ -255,6 +327,14 @@ interface EngineOptions {
|
|
|
255
327
|
* lever. Pass `false` to opt out (pure deterministic eval).
|
|
256
328
|
*/
|
|
257
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;
|
|
258
338
|
/**
|
|
259
339
|
* Test mode — no network at all. identify()/init are no-ops (never call
|
|
260
340
|
* /sdk/evaluate), track() is a no-op, telemetry is forced off, and the client
|
|
@@ -275,6 +355,7 @@ declare class Engine {
|
|
|
275
355
|
private readonly env;
|
|
276
356
|
private evalResult;
|
|
277
357
|
private anonId;
|
|
358
|
+
private anonReady;
|
|
278
359
|
private userId;
|
|
279
360
|
private buffer;
|
|
280
361
|
private telemetry;
|
|
@@ -289,6 +370,13 @@ declare class Engine {
|
|
|
289
370
|
private readonly experimentOverrides;
|
|
290
371
|
private onOverrideChange;
|
|
291
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;
|
|
292
380
|
/**
|
|
293
381
|
* Build a no-network, immediately-usable browser client for tests
|
|
294
382
|
* (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
|
|
@@ -342,16 +430,21 @@ declare class Engine {
|
|
|
342
430
|
getFlag(name: string, defaultValue?: boolean): boolean;
|
|
343
431
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
344
432
|
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
345
|
-
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
|
|
346
|
-
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, opts: GetExperimentOptions<P>): ExperimentResult<P>;
|
|
347
433
|
/**
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
* the
|
|
351
|
-
*
|
|
352
|
-
* `
|
|
353
|
-
|
|
354
|
-
|
|
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;
|
|
355
448
|
/**
|
|
356
449
|
* Subscribe to state changes — fires after identify() completes and on
|
|
357
450
|
* `se:override:change` events from the devtools overlay. Returns an
|
|
@@ -468,12 +561,26 @@ interface ShipeasyClientConfig {
|
|
|
468
561
|
autoCollect?: boolean | (Partial<AutoCollectGroups> & {
|
|
469
562
|
always?: boolean;
|
|
470
563
|
});
|
|
564
|
+
/**
|
|
565
|
+
* How chatty the SDK is on `console` when it swallows an internal error.
|
|
566
|
+
* `silent` < `error` < `warn` < `info` < `debug`; defaults to `"warn"`. Every
|
|
567
|
+
* runtime read/track/see call fails silently to a safe default and reports the
|
|
568
|
+
* cause at this level. See {@link EngineOptions.logLevel}.
|
|
569
|
+
*/
|
|
570
|
+
logLevel?: LogLevel;
|
|
471
571
|
/**
|
|
472
572
|
* Disable per-evaluation usage telemetry. Telemetry is ON by default — every
|
|
473
573
|
* flag/config/experiment/killswitch read fires one fire-and-forget beacon
|
|
474
574
|
* counted by Cloudflare's native per-path analytics. Pass `true` to opt out.
|
|
475
575
|
*/
|
|
476
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;
|
|
477
584
|
/**
|
|
478
585
|
* Suppress automatic exposure logging in `flags.getExperiment` (Statsig's
|
|
479
586
|
* `disableExposureLogging`). Default false. When true, call
|
|
@@ -493,6 +600,12 @@ interface ShipeasyClientConfig {
|
|
|
493
600
|
* opt out. See {@link EngineOptions.stickyBucketing}.
|
|
494
601
|
*/
|
|
495
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;
|
|
496
609
|
}
|
|
497
610
|
/**
|
|
498
611
|
* Initialise the ShipEasy client SDK and wire up lazy devtools.
|
|
@@ -552,10 +665,19 @@ declare const flags: {
|
|
|
552
665
|
/** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
|
|
553
666
|
getDetail(name: string): FlagDetail;
|
|
554
667
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
*
|
|
558
|
-
|
|
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
|
+
};
|
|
559
681
|
track(eventName: string, props?: Record<string, unknown>): void;
|
|
560
682
|
/**
|
|
561
683
|
* Read a killswitch. Without `switchKey`, returns true when the killswitch is
|
|
@@ -651,7 +773,17 @@ declare class Client<U = unknown> {
|
|
|
651
773
|
getFlagDetail(name: string): FlagDetail;
|
|
652
774
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
653
775
|
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
654
|
-
|
|
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
|
+
};
|
|
655
787
|
/** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
|
|
656
788
|
getKillswitch(name: string, switchKey?: string): boolean;
|
|
657
789
|
/**
|
|
@@ -661,13 +793,6 @@ declare class Client<U = unknown> {
|
|
|
661
793
|
* test mode.
|
|
662
794
|
*/
|
|
663
795
|
track(eventName: string, props?: Record<string, unknown>): void;
|
|
664
|
-
/**
|
|
665
|
-
* Log an exposure for `name` at the treatment's render for the bound user.
|
|
666
|
-
* Delegates to {@link Engine.logExposure} (no-op when the visitor isn't
|
|
667
|
-
* enrolled). Pair with `getExperiment(name, …, { logExposure: false })` /
|
|
668
|
-
* `disableAutoExposure` to log exposure exactly when you render.
|
|
669
|
-
*/
|
|
670
|
-
logExposure(name: string): void;
|
|
671
796
|
}
|
|
672
797
|
interface SeeApi {
|
|
673
798
|
/**
|
|
@@ -760,4 +885,4 @@ interface I18nFacade {
|
|
|
760
885
|
}
|
|
761
886
|
declare const i18n: I18nFacade;
|
|
762
887
|
|
|
763
|
-
export { type AttributesFn, type AutoCollectGroups, type BootstrapPayload, Client, type ConfigureOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, FLAG_REASONS, type FlagDetail, type FlagReason, type GetConfigOptions, type GetExperimentOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetConfigureForTests, _resetShipeasyForTests, attachDevtools, configure, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
|
|
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 };
|