@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/dist/server/index.d.mts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
|
|
3
|
+
type LogLevel = "silent" | "error" | "warn" | "info" | "debug";
|
|
4
|
+
/** All accepted {@link LogLevel} values, in increasing verbosity. */
|
|
5
|
+
declare const LOG_LEVELS: readonly LogLevel[];
|
|
6
|
+
|
|
3
7
|
type SeeExtras = Record<string, string | number | boolean | null | undefined>;
|
|
4
8
|
type SeeKind = "caught" | "uncaught" | "unhandled_rejection" | "network" | "violation";
|
|
5
9
|
/** Built by `causesThe(subject).to(outcome)` — never constructed by hand. */
|
|
@@ -121,6 +125,10 @@ interface ExperimentResult<P> {
|
|
|
121
125
|
inExperiment: boolean;
|
|
122
126
|
group: string;
|
|
123
127
|
params: P;
|
|
128
|
+
/** The universe this experiment belongs to. Carried in the SSR bootstrap +
|
|
129
|
+
* `/sdk/evaluate` so the client can resolve `universe(name).assign()` by
|
|
130
|
+
* finding the enrolled experiment in a universe. */
|
|
131
|
+
universe?: string;
|
|
124
132
|
}
|
|
125
133
|
/**
|
|
126
134
|
* Why a flag evaluated the way it did (LaunchDarkly variationDetail parity).
|
|
@@ -166,13 +174,46 @@ interface ExperimentGroup {
|
|
|
166
174
|
interface Experiment {
|
|
167
175
|
universe: string;
|
|
168
176
|
targetingGate?: string | null;
|
|
177
|
+
/**
|
|
178
|
+
* Per-experiment holdout flag (a `type='holdout'` gate). When set and the flag
|
|
179
|
+
* passes for a unit, that unit is *held out* of THIS experiment — never
|
|
180
|
+
* assigned, sees the universe defaults. Checked after the universe holdout,
|
|
181
|
+
* before allocation. Mirrors @shipeasy/core §B3.
|
|
182
|
+
*/
|
|
183
|
+
holdoutGate?: string | null;
|
|
169
184
|
allocationPct: number;
|
|
185
|
+
/**
|
|
186
|
+
* Contiguous half-open slice `[poolOffsetBp, poolOffsetBp + poolSizeBp)` of the
|
|
187
|
+
* *universe* hash space this experiment claims. Used only when `hashVersion >= 2`
|
|
188
|
+
* (pooled assignment / real mutual exclusion, §B4): the unit's universe segment
|
|
189
|
+
* must fall in this range to be in the experiment. Null ⇒ fall back to the
|
|
190
|
+
* legacy independent-salt `allocationPct` gate.
|
|
191
|
+
*/
|
|
192
|
+
poolOffsetBp?: number | null;
|
|
193
|
+
poolSizeBp?: number | null;
|
|
194
|
+
/**
|
|
195
|
+
* A tail of the group split kept empty (basis points) so a new variant can be
|
|
196
|
+
* appended into it while running without reshuffling enrolled units. Group
|
|
197
|
+
* weights sum to `10000 − reservedHeadroomBp`; a unit hashing into the reserved
|
|
198
|
+
* tail is treated as not-assigned (sees universe defaults). §B5.
|
|
199
|
+
*/
|
|
200
|
+
reservedHeadroomBp?: number | null;
|
|
170
201
|
salt: string;
|
|
171
202
|
groups: ExperimentGroup[];
|
|
172
203
|
status: "draft" | "running" | "stopped" | "archived";
|
|
204
|
+
/** Bucketing scheme version. `>= 2` unlocks pooled (mutually-exclusive) assignment. */
|
|
205
|
+
hashVersion?: number;
|
|
173
206
|
/** Attribute to bucket on (e.g. company_id); defaults to user_id/anonymous_id. */
|
|
174
207
|
bucketBy?: string | null;
|
|
175
208
|
}
|
|
209
|
+
/** One param the universe owns: a name, a type, and the default a variant
|
|
210
|
+
* inherits when it doesn't override the value (§A/§B2). */
|
|
211
|
+
interface UniverseParam {
|
|
212
|
+
name: string;
|
|
213
|
+
type: "bool" | "int" | "number" | "string";
|
|
214
|
+
default: unknown;
|
|
215
|
+
}
|
|
216
|
+
type UniverseParamSchema = UniverseParam[];
|
|
176
217
|
/** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
|
|
177
218
|
interface StickyEntry {
|
|
178
219
|
g: string;
|
|
@@ -193,6 +234,13 @@ interface StickyBucketStore {
|
|
|
193
234
|
declare function createInMemoryStickyStore(seed?: Record<string, Record<string, StickyEntry>>): StickyBucketStore;
|
|
194
235
|
interface Universe {
|
|
195
236
|
holdout_range: [number, number] | null;
|
|
237
|
+
/**
|
|
238
|
+
* The universe's config schema — the single source of truth for param names,
|
|
239
|
+
* types, and defaults (§A). The `assign()` merge layers these defaults *under*
|
|
240
|
+
* an assigned variant's overrides so an unset param still returns the universe
|
|
241
|
+
* default. Null/absent ⇒ no defaults (legacy: variant params returned verbatim).
|
|
242
|
+
*/
|
|
243
|
+
param_schema?: UniverseParamSchema | null;
|
|
196
244
|
}
|
|
197
245
|
interface Killswitch {
|
|
198
246
|
killed: 0 | 1 | boolean;
|
|
@@ -219,8 +267,36 @@ interface BootstrapPayload {
|
|
|
219
267
|
configs: Record<string, unknown>;
|
|
220
268
|
experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
|
|
221
269
|
killswitches: Record<string, boolean | Record<string, boolean>>;
|
|
270
|
+
/** Per-universe param defaults (name → default map) so the client can resolve
|
|
271
|
+
* `universe(name).get(field)` to the universe default even when the unit is
|
|
272
|
+
* not enrolled in any experiment there. Only universes with running
|
|
273
|
+
* experiments are included. */
|
|
274
|
+
universes?: Record<string, {
|
|
275
|
+
defaults: Record<string, unknown>;
|
|
276
|
+
}>;
|
|
222
277
|
}
|
|
223
278
|
declare const ANON_ID_COOKIE = "__se_anon_id";
|
|
279
|
+
/**
|
|
280
|
+
* The result of `universe(name).assign(user)` — a user's standing in a universe.
|
|
281
|
+
* A universe is a mutual-exclusion pool, so a unit lands in **at most one**
|
|
282
|
+
* experiment. Never throws: an un-enrolled unit still resolves `get()` to the
|
|
283
|
+
* universe defaults (or your fallback). Reading is side-effect free — the single
|
|
284
|
+
* exposure is logged once by `assign()` when the unit is enrolled.
|
|
285
|
+
*/
|
|
286
|
+
interface Assignment {
|
|
287
|
+
/** The experiment the unit landed in, or `null` when not enrolled. */
|
|
288
|
+
readonly name: string | null;
|
|
289
|
+
/** The assigned variant/group name, or `null` when not enrolled. */
|
|
290
|
+
readonly group: string | null;
|
|
291
|
+
/** True iff the unit is enrolled in an experiment in this universe. */
|
|
292
|
+
readonly enrolled: boolean;
|
|
293
|
+
/**
|
|
294
|
+
* Read a resolved param: the assigned variant's override, else the universe
|
|
295
|
+
* default, else `fallback`. Works even when not enrolled (variant layer is
|
|
296
|
+
* absent, so you get `universeDefault ?? fallback`).
|
|
297
|
+
*/
|
|
298
|
+
get<T = unknown>(field: string, fallback?: T): T | undefined;
|
|
299
|
+
}
|
|
224
300
|
type EngineEnv = "dev" | "staging" | "prod";
|
|
225
301
|
interface EngineOptions {
|
|
226
302
|
apiKey: string;
|
|
@@ -243,6 +319,24 @@ interface EngineOptions {
|
|
|
243
319
|
disableTelemetry?: boolean;
|
|
244
320
|
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
245
321
|
telemetryUrl?: string;
|
|
322
|
+
/**
|
|
323
|
+
* How chatty the SDK is on `console` when it catches an error internally.
|
|
324
|
+
* Every public runtime method (getFlag/getConfig/getExperiment/getKillswitch/
|
|
325
|
+
* track/logExposure/see) fails silently — it returns a safe default rather
|
|
326
|
+
* than throwing — and surfaces the swallowed error through this level so you
|
|
327
|
+
* still find out. Ordering: `silent` < `error` < `warn` < `info` < `debug`.
|
|
328
|
+
* Defaults to `"warn"` (prints `error` + `warn`). Pass `"silent"` to mute the
|
|
329
|
+
* SDK entirely. See {@link LogLevel}.
|
|
330
|
+
*/
|
|
331
|
+
logLevel?: LogLevel;
|
|
332
|
+
/**
|
|
333
|
+
* Opt out of SDK-internal error self-monitoring. When one of the SDK's own
|
|
334
|
+
* last-resort guards catches an internal failure (an "on our end" bug), the
|
|
335
|
+
* SDK reports it to Shipeasy's own project so we can track SDK bugs across
|
|
336
|
+
* apps — never to your project. ON by default; forced off in test mode. Pass
|
|
337
|
+
* `true` to disable.
|
|
338
|
+
*/
|
|
339
|
+
disableInternalErrorReporting?: boolean;
|
|
246
340
|
/**
|
|
247
341
|
* Attribute names usable for targeting but never persisted in analytics
|
|
248
342
|
* (LD/Statsig `privateAttributes`). The server evaluates locally so private
|
|
@@ -285,6 +379,7 @@ declare class Engine {
|
|
|
285
379
|
private readonly flagOverrides;
|
|
286
380
|
private readonly configOverrides;
|
|
287
381
|
private readonly experimentOverrides;
|
|
382
|
+
private readonly exposureSeen;
|
|
288
383
|
private readonly changeListeners;
|
|
289
384
|
constructor(opts: EngineOptions);
|
|
290
385
|
/**
|
|
@@ -364,19 +459,47 @@ declare class Engine {
|
|
|
364
459
|
getFlag(name: string, user: User, defaultValue?: boolean): boolean;
|
|
365
460
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
366
461
|
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
367
|
-
|
|
462
|
+
/**
|
|
463
|
+
* Bind the per-experiment sticky seam over the configured {@link StickyBucketStore}
|
|
464
|
+
* for one (unit, experiment). Absent store ⇒ deterministic (no sticky).
|
|
465
|
+
*/
|
|
466
|
+
private bindSticky;
|
|
467
|
+
/**
|
|
468
|
+
* Evaluate one experiment by name for `user` — override → full classify
|
|
469
|
+
* pipeline (targeting → universe holdout → holdout gate → sticky → allocation
|
|
470
|
+
* → group), merging the universe defaults under the assigned variant (§B2).
|
|
471
|
+
* Internal: the public surface is `universe(name).assign(user)`. Reused by the
|
|
472
|
+
* SSR `evaluate()` bootstrap (keyed by experiment name) and by `assignUniverse`.
|
|
473
|
+
*/
|
|
474
|
+
private evalExperiment;
|
|
475
|
+
/**
|
|
476
|
+
* Assign `user` within `universeName`. A universe is a mutual-exclusion pool,
|
|
477
|
+
* so a unit lands in **at most one** experiment; the returned {@link Assignment}
|
|
478
|
+
* exposes the variant + resolved params and auto-logs a single exposure when
|
|
479
|
+
* enrolled. An un-enrolled unit still resolves `get()` to the universe defaults.
|
|
480
|
+
* Never throws. This is the sole experiment read path (there is no
|
|
481
|
+
* `getExperiment` — a caller asks a universe, not an experiment).
|
|
482
|
+
*/
|
|
483
|
+
assignUniverse(universeName: string, user: User): Assignment;
|
|
484
|
+
/**
|
|
485
|
+
* The universe-first experiment read entry point:
|
|
486
|
+
* `engine.universe("checkout").assign(user)`. Returns a reusable handle bound
|
|
487
|
+
* to one universe; `assign(user)` picks the ≤1 experiment the unit is pooled
|
|
488
|
+
* into and auto-logs a single exposure. See {@link assignUniverse}.
|
|
489
|
+
*/
|
|
490
|
+
universe(name: string): {
|
|
491
|
+
assign(user: User): Assignment;
|
|
492
|
+
};
|
|
368
493
|
/** Drop caller-marked private attributes from an outbound props bag. */
|
|
369
494
|
private stripPrivate;
|
|
370
495
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
371
496
|
/**
|
|
372
|
-
*
|
|
373
|
-
* (
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
* as `{ user_id }`); if the user is enrolled, POSTs a single exposure to
|
|
377
|
-
* `/collect`. No-op in test mode or when the user isn't enrolled.
|
|
497
|
+
* POST a single exposure for an enrolled `(user, experiment, group)`. Deduped
|
|
498
|
+
* per process (bounded set) so repeated `assign()` calls in one server don't
|
|
499
|
+
* spam `/collect`. Fire-and-forget; no-op in test mode. This is how
|
|
500
|
+
* `assignUniverse` auto-logs — the browser's auto-exposure parity for SSR.
|
|
378
501
|
*/
|
|
379
|
-
|
|
502
|
+
private postExposure;
|
|
380
503
|
/**
|
|
381
504
|
* Report a structured error into the errors primitive. Fire-and-forget —
|
|
382
505
|
* never blocks or throws into the request path. Spam-guarded by a 30s
|
|
@@ -460,12 +583,26 @@ interface ShipeasyServerConfig {
|
|
|
460
583
|
user?: User;
|
|
461
584
|
/** i18n profile to load for SSR translations, e.g. "en:prod". Defaults to "en:prod". */
|
|
462
585
|
i18nDefaultProfile?: string;
|
|
586
|
+
/**
|
|
587
|
+
* How chatty the SDK is on `console` when it swallows an internal error.
|
|
588
|
+
* `silent` < `error` < `warn` < `info` < `debug`; defaults to `"warn"`. Every
|
|
589
|
+
* runtime read/track/see call fails silently to a safe default and reports the
|
|
590
|
+
* cause at this level. See {@link EngineOptions.logLevel}.
|
|
591
|
+
*/
|
|
592
|
+
logLevel?: LogLevel;
|
|
463
593
|
/**
|
|
464
594
|
* Disable per-evaluation usage telemetry. ON by default. On Cloudflare
|
|
465
595
|
* Workers each beacon is an outbound subrequest, so disable on hot SSR paths
|
|
466
596
|
* that evaluate many flags per request. See {@link EngineOptions.disableTelemetry}.
|
|
467
597
|
*/
|
|
468
598
|
disableTelemetry?: boolean;
|
|
599
|
+
/**
|
|
600
|
+
* Opt out of SDK-internal error self-monitoring. Internal SDK failures ("on
|
|
601
|
+
* our end") are reported to Shipeasy's own project (never yours) so we can
|
|
602
|
+
* track SDK bugs. ON by default. See
|
|
603
|
+
* {@link EngineOptions.disableInternalErrorReporting}.
|
|
604
|
+
*/
|
|
605
|
+
disableInternalErrorReporting?: boolean;
|
|
469
606
|
/**
|
|
470
607
|
* Attribute names usable for targeting but never persisted in analytics
|
|
471
608
|
* (LD/Statsig `privateAttributes`). Stripped from every outbound `track()`
|
|
@@ -561,7 +698,17 @@ declare const flags: {
|
|
|
561
698
|
/** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
|
|
562
699
|
getDetail(name: string, user: User): FlagDetail;
|
|
563
700
|
getConfig<T = unknown>(name: string, decodeOrOpts?: ((raw: unknown) => T) | GetConfigOptions<T>): T | undefined;
|
|
564
|
-
|
|
701
|
+
/**
|
|
702
|
+
* Assign `user` within a universe: `flags.universe("checkout").assign(user)`.
|
|
703
|
+
* A universe is a mutual-exclusion pool, so the unit lands in ≤1 experiment;
|
|
704
|
+
* the returned {@link Assignment} exposes `.group` / `.get(field, fallback)`
|
|
705
|
+
* and auto-logs one exposure when enrolled. Before configure() (or on any
|
|
706
|
+
* error) it returns a safe not-enrolled handle. This replaces the removed
|
|
707
|
+
* `getExperiment` — read experiments by universe, never by name.
|
|
708
|
+
*/
|
|
709
|
+
universe(name: string): {
|
|
710
|
+
assign(user: User): Assignment;
|
|
711
|
+
};
|
|
565
712
|
/**
|
|
566
713
|
* Read a killswitch. Without `switchKey`, returns true when the whole
|
|
567
714
|
* killswitch is killed. With `switchKey`, returns true when that specific
|
|
@@ -569,9 +716,6 @@ declare const flags: {
|
|
|
569
716
|
*/
|
|
570
717
|
ks(name: string, switchKey?: string): boolean;
|
|
571
718
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
572
|
-
/** Emit an exposure for an enrolled experiment at the decision point. See
|
|
573
|
-
* {@link Engine.logExposure}. No-op before configure(). */
|
|
574
|
-
logExposure(user: string | User, name: string): void;
|
|
575
719
|
/**
|
|
576
720
|
* Evaluate all flags / configs / experiments for a user against the locally
|
|
577
721
|
* cached blob. Pass the request URL to apply ?se_ks_* / ?se_cf_* / ?se_exp_*
|
|
@@ -719,7 +863,15 @@ declare class Client<U = unknown> {
|
|
|
719
863
|
getFlagDetail(name: string): FlagDetail;
|
|
720
864
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
721
865
|
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
722
|
-
|
|
866
|
+
/**
|
|
867
|
+
* Assign the bound user within a universe: `client.universe("checkout").assign()`.
|
|
868
|
+
* The user is already bound at construction, so `assign()` takes no arg. Returns
|
|
869
|
+
* an {@link Assignment} (`.group` / `.get(field, fallback)`) and auto-logs one
|
|
870
|
+
* exposure when enrolled. Replaces the removed `getExperiment`.
|
|
871
|
+
*/
|
|
872
|
+
universe(name: string): {
|
|
873
|
+
assign(): Assignment;
|
|
874
|
+
};
|
|
723
875
|
/** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
|
|
724
876
|
getKillswitch(name: string, switchKey?: string): boolean;
|
|
725
877
|
/**
|
|
@@ -730,13 +882,6 @@ declare class Client<U = unknown> {
|
|
|
730
882
|
* mode.
|
|
731
883
|
*/
|
|
732
884
|
track(eventName: string, props?: Record<string, unknown>): void;
|
|
733
|
-
/**
|
|
734
|
-
* Emit an exposure event for `name` at this server-side decision point for the
|
|
735
|
-
* bound user. Delegates to {@link Engine.logExposure} with the resolved
|
|
736
|
-
* attribute bag (re-evaluates enrolment; no-op when the user isn't enrolled or
|
|
737
|
-
* in test mode).
|
|
738
|
-
*/
|
|
739
|
-
logExposure(name: string): void;
|
|
740
885
|
}
|
|
741
886
|
interface SeeApi {
|
|
742
887
|
/**
|
|
@@ -789,4 +934,4 @@ interface SeeApi {
|
|
|
789
934
|
*/
|
|
790
935
|
declare const see: SeeApi;
|
|
791
936
|
|
|
792
|
-
export { ANON_ID_COOKIE, type AttributesFn, type BootstrapData, type BootstrapEmitOptions, type BootstrapPayload, Client, type ConfigureOfflineOptions, type ConfigureOptions, type ConfigureTestOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, type ExpsBlob, FLAG_REASONS, type FetchLabelsOptions, type FlagDetail, type FlagReason, type FlagsBlob, type GetConfigOptions, type I18nForRequest, type LabelFile, type ScriptTagSpec, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type StickyBucketStore, type StickyEntry, type User, type Violation, _murmur3ForTests, _resetConfigureForTests, _resetShipeasyServerForTests, clearOverrides, configure, configureForOffline, configureForTesting, configureShipeasyServer, createInMemoryStickyStore, fetchLabelsForSSR, flags, getBootstrapData, getBootstrapTags, getShipeasyServerClient, i18n, isExpected, onChange, overrideConfig, overrideExperiment, overrideFlag, see, seeContext, shipeasy, version };
|
|
937
|
+
export { ANON_ID_COOKIE, type Assignment, type AttributesFn, type BootstrapData, type BootstrapEmitOptions, type BootstrapPayload, Client, type ConfigureOfflineOptions, type ConfigureOptions, type ConfigureTestOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, type ExpsBlob, FLAG_REASONS, type FetchLabelsOptions, type FlagDetail, type FlagReason, type FlagsBlob, type GetConfigOptions, type I18nForRequest, LOG_LEVELS, type LabelFile, type LogLevel, type ScriptTagSpec, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type StickyBucketStore, type StickyEntry, type UniverseParam, type UniverseParamSchema, type User, type Violation, _murmur3ForTests, _resetConfigureForTests, _resetShipeasyServerForTests, clearOverrides, configure, configureForOffline, configureForTesting, configureShipeasyServer, createInMemoryStickyStore, fetchLabelsForSSR, flags, getBootstrapData, getBootstrapTags, getShipeasyServerClient, i18n, isExpected, onChange, overrideConfig, overrideExperiment, overrideFlag, see, seeContext, shipeasy, version };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
|
|
3
|
+
type LogLevel = "silent" | "error" | "warn" | "info" | "debug";
|
|
4
|
+
/** All accepted {@link LogLevel} values, in increasing verbosity. */
|
|
5
|
+
declare const LOG_LEVELS: readonly LogLevel[];
|
|
6
|
+
|
|
3
7
|
type SeeExtras = Record<string, string | number | boolean | null | undefined>;
|
|
4
8
|
type SeeKind = "caught" | "uncaught" | "unhandled_rejection" | "network" | "violation";
|
|
5
9
|
/** Built by `causesThe(subject).to(outcome)` — never constructed by hand. */
|
|
@@ -121,6 +125,10 @@ interface ExperimentResult<P> {
|
|
|
121
125
|
inExperiment: boolean;
|
|
122
126
|
group: string;
|
|
123
127
|
params: P;
|
|
128
|
+
/** The universe this experiment belongs to. Carried in the SSR bootstrap +
|
|
129
|
+
* `/sdk/evaluate` so the client can resolve `universe(name).assign()` by
|
|
130
|
+
* finding the enrolled experiment in a universe. */
|
|
131
|
+
universe?: string;
|
|
124
132
|
}
|
|
125
133
|
/**
|
|
126
134
|
* Why a flag evaluated the way it did (LaunchDarkly variationDetail parity).
|
|
@@ -166,13 +174,46 @@ interface ExperimentGroup {
|
|
|
166
174
|
interface Experiment {
|
|
167
175
|
universe: string;
|
|
168
176
|
targetingGate?: string | null;
|
|
177
|
+
/**
|
|
178
|
+
* Per-experiment holdout flag (a `type='holdout'` gate). When set and the flag
|
|
179
|
+
* passes for a unit, that unit is *held out* of THIS experiment — never
|
|
180
|
+
* assigned, sees the universe defaults. Checked after the universe holdout,
|
|
181
|
+
* before allocation. Mirrors @shipeasy/core §B3.
|
|
182
|
+
*/
|
|
183
|
+
holdoutGate?: string | null;
|
|
169
184
|
allocationPct: number;
|
|
185
|
+
/**
|
|
186
|
+
* Contiguous half-open slice `[poolOffsetBp, poolOffsetBp + poolSizeBp)` of the
|
|
187
|
+
* *universe* hash space this experiment claims. Used only when `hashVersion >= 2`
|
|
188
|
+
* (pooled assignment / real mutual exclusion, §B4): the unit's universe segment
|
|
189
|
+
* must fall in this range to be in the experiment. Null ⇒ fall back to the
|
|
190
|
+
* legacy independent-salt `allocationPct` gate.
|
|
191
|
+
*/
|
|
192
|
+
poolOffsetBp?: number | null;
|
|
193
|
+
poolSizeBp?: number | null;
|
|
194
|
+
/**
|
|
195
|
+
* A tail of the group split kept empty (basis points) so a new variant can be
|
|
196
|
+
* appended into it while running without reshuffling enrolled units. Group
|
|
197
|
+
* weights sum to `10000 − reservedHeadroomBp`; a unit hashing into the reserved
|
|
198
|
+
* tail is treated as not-assigned (sees universe defaults). §B5.
|
|
199
|
+
*/
|
|
200
|
+
reservedHeadroomBp?: number | null;
|
|
170
201
|
salt: string;
|
|
171
202
|
groups: ExperimentGroup[];
|
|
172
203
|
status: "draft" | "running" | "stopped" | "archived";
|
|
204
|
+
/** Bucketing scheme version. `>= 2` unlocks pooled (mutually-exclusive) assignment. */
|
|
205
|
+
hashVersion?: number;
|
|
173
206
|
/** Attribute to bucket on (e.g. company_id); defaults to user_id/anonymous_id. */
|
|
174
207
|
bucketBy?: string | null;
|
|
175
208
|
}
|
|
209
|
+
/** One param the universe owns: a name, a type, and the default a variant
|
|
210
|
+
* inherits when it doesn't override the value (§A/§B2). */
|
|
211
|
+
interface UniverseParam {
|
|
212
|
+
name: string;
|
|
213
|
+
type: "bool" | "int" | "number" | "string";
|
|
214
|
+
default: unknown;
|
|
215
|
+
}
|
|
216
|
+
type UniverseParamSchema = UniverseParam[];
|
|
176
217
|
/** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
|
|
177
218
|
interface StickyEntry {
|
|
178
219
|
g: string;
|
|
@@ -193,6 +234,13 @@ interface StickyBucketStore {
|
|
|
193
234
|
declare function createInMemoryStickyStore(seed?: Record<string, Record<string, StickyEntry>>): StickyBucketStore;
|
|
194
235
|
interface Universe {
|
|
195
236
|
holdout_range: [number, number] | null;
|
|
237
|
+
/**
|
|
238
|
+
* The universe's config schema — the single source of truth for param names,
|
|
239
|
+
* types, and defaults (§A). The `assign()` merge layers these defaults *under*
|
|
240
|
+
* an assigned variant's overrides so an unset param still returns the universe
|
|
241
|
+
* default. Null/absent ⇒ no defaults (legacy: variant params returned verbatim).
|
|
242
|
+
*/
|
|
243
|
+
param_schema?: UniverseParamSchema | null;
|
|
196
244
|
}
|
|
197
245
|
interface Killswitch {
|
|
198
246
|
killed: 0 | 1 | boolean;
|
|
@@ -219,8 +267,36 @@ interface BootstrapPayload {
|
|
|
219
267
|
configs: Record<string, unknown>;
|
|
220
268
|
experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
|
|
221
269
|
killswitches: Record<string, boolean | Record<string, boolean>>;
|
|
270
|
+
/** Per-universe param defaults (name → default map) so the client can resolve
|
|
271
|
+
* `universe(name).get(field)` to the universe default even when the unit is
|
|
272
|
+
* not enrolled in any experiment there. Only universes with running
|
|
273
|
+
* experiments are included. */
|
|
274
|
+
universes?: Record<string, {
|
|
275
|
+
defaults: Record<string, unknown>;
|
|
276
|
+
}>;
|
|
222
277
|
}
|
|
223
278
|
declare const ANON_ID_COOKIE = "__se_anon_id";
|
|
279
|
+
/**
|
|
280
|
+
* The result of `universe(name).assign(user)` — a user's standing in a universe.
|
|
281
|
+
* A universe is a mutual-exclusion pool, so a unit lands in **at most one**
|
|
282
|
+
* experiment. Never throws: an un-enrolled unit still resolves `get()` to the
|
|
283
|
+
* universe defaults (or your fallback). Reading is side-effect free — the single
|
|
284
|
+
* exposure is logged once by `assign()` when the unit is enrolled.
|
|
285
|
+
*/
|
|
286
|
+
interface Assignment {
|
|
287
|
+
/** The experiment the unit landed in, or `null` when not enrolled. */
|
|
288
|
+
readonly name: string | null;
|
|
289
|
+
/** The assigned variant/group name, or `null` when not enrolled. */
|
|
290
|
+
readonly group: string | null;
|
|
291
|
+
/** True iff the unit is enrolled in an experiment in this universe. */
|
|
292
|
+
readonly enrolled: boolean;
|
|
293
|
+
/**
|
|
294
|
+
* Read a resolved param: the assigned variant's override, else the universe
|
|
295
|
+
* default, else `fallback`. Works even when not enrolled (variant layer is
|
|
296
|
+
* absent, so you get `universeDefault ?? fallback`).
|
|
297
|
+
*/
|
|
298
|
+
get<T = unknown>(field: string, fallback?: T): T | undefined;
|
|
299
|
+
}
|
|
224
300
|
type EngineEnv = "dev" | "staging" | "prod";
|
|
225
301
|
interface EngineOptions {
|
|
226
302
|
apiKey: string;
|
|
@@ -243,6 +319,24 @@ interface EngineOptions {
|
|
|
243
319
|
disableTelemetry?: boolean;
|
|
244
320
|
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
245
321
|
telemetryUrl?: string;
|
|
322
|
+
/**
|
|
323
|
+
* How chatty the SDK is on `console` when it catches an error internally.
|
|
324
|
+
* Every public runtime method (getFlag/getConfig/getExperiment/getKillswitch/
|
|
325
|
+
* track/logExposure/see) fails silently — it returns a safe default rather
|
|
326
|
+
* than throwing — and surfaces the swallowed error through this level so you
|
|
327
|
+
* still find out. Ordering: `silent` < `error` < `warn` < `info` < `debug`.
|
|
328
|
+
* Defaults to `"warn"` (prints `error` + `warn`). Pass `"silent"` to mute the
|
|
329
|
+
* SDK entirely. See {@link LogLevel}.
|
|
330
|
+
*/
|
|
331
|
+
logLevel?: LogLevel;
|
|
332
|
+
/**
|
|
333
|
+
* Opt out of SDK-internal error self-monitoring. When one of the SDK's own
|
|
334
|
+
* last-resort guards catches an internal failure (an "on our end" bug), the
|
|
335
|
+
* SDK reports it to Shipeasy's own project so we can track SDK bugs across
|
|
336
|
+
* apps — never to your project. ON by default; forced off in test mode. Pass
|
|
337
|
+
* `true` to disable.
|
|
338
|
+
*/
|
|
339
|
+
disableInternalErrorReporting?: boolean;
|
|
246
340
|
/**
|
|
247
341
|
* Attribute names usable for targeting but never persisted in analytics
|
|
248
342
|
* (LD/Statsig `privateAttributes`). The server evaluates locally so private
|
|
@@ -285,6 +379,7 @@ declare class Engine {
|
|
|
285
379
|
private readonly flagOverrides;
|
|
286
380
|
private readonly configOverrides;
|
|
287
381
|
private readonly experimentOverrides;
|
|
382
|
+
private readonly exposureSeen;
|
|
288
383
|
private readonly changeListeners;
|
|
289
384
|
constructor(opts: EngineOptions);
|
|
290
385
|
/**
|
|
@@ -364,19 +459,47 @@ declare class Engine {
|
|
|
364
459
|
getFlag(name: string, user: User, defaultValue?: boolean): boolean;
|
|
365
460
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
366
461
|
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
367
|
-
|
|
462
|
+
/**
|
|
463
|
+
* Bind the per-experiment sticky seam over the configured {@link StickyBucketStore}
|
|
464
|
+
* for one (unit, experiment). Absent store ⇒ deterministic (no sticky).
|
|
465
|
+
*/
|
|
466
|
+
private bindSticky;
|
|
467
|
+
/**
|
|
468
|
+
* Evaluate one experiment by name for `user` — override → full classify
|
|
469
|
+
* pipeline (targeting → universe holdout → holdout gate → sticky → allocation
|
|
470
|
+
* → group), merging the universe defaults under the assigned variant (§B2).
|
|
471
|
+
* Internal: the public surface is `universe(name).assign(user)`. Reused by the
|
|
472
|
+
* SSR `evaluate()` bootstrap (keyed by experiment name) and by `assignUniverse`.
|
|
473
|
+
*/
|
|
474
|
+
private evalExperiment;
|
|
475
|
+
/**
|
|
476
|
+
* Assign `user` within `universeName`. A universe is a mutual-exclusion pool,
|
|
477
|
+
* so a unit lands in **at most one** experiment; the returned {@link Assignment}
|
|
478
|
+
* exposes the variant + resolved params and auto-logs a single exposure when
|
|
479
|
+
* enrolled. An un-enrolled unit still resolves `get()` to the universe defaults.
|
|
480
|
+
* Never throws. This is the sole experiment read path (there is no
|
|
481
|
+
* `getExperiment` — a caller asks a universe, not an experiment).
|
|
482
|
+
*/
|
|
483
|
+
assignUniverse(universeName: string, user: User): Assignment;
|
|
484
|
+
/**
|
|
485
|
+
* The universe-first experiment read entry point:
|
|
486
|
+
* `engine.universe("checkout").assign(user)`. Returns a reusable handle bound
|
|
487
|
+
* to one universe; `assign(user)` picks the ≤1 experiment the unit is pooled
|
|
488
|
+
* into and auto-logs a single exposure. See {@link assignUniverse}.
|
|
489
|
+
*/
|
|
490
|
+
universe(name: string): {
|
|
491
|
+
assign(user: User): Assignment;
|
|
492
|
+
};
|
|
368
493
|
/** Drop caller-marked private attributes from an outbound props bag. */
|
|
369
494
|
private stripPrivate;
|
|
370
495
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
371
496
|
/**
|
|
372
|
-
*
|
|
373
|
-
* (
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
* as `{ user_id }`); if the user is enrolled, POSTs a single exposure to
|
|
377
|
-
* `/collect`. No-op in test mode or when the user isn't enrolled.
|
|
497
|
+
* POST a single exposure for an enrolled `(user, experiment, group)`. Deduped
|
|
498
|
+
* per process (bounded set) so repeated `assign()` calls in one server don't
|
|
499
|
+
* spam `/collect`. Fire-and-forget; no-op in test mode. This is how
|
|
500
|
+
* `assignUniverse` auto-logs — the browser's auto-exposure parity for SSR.
|
|
378
501
|
*/
|
|
379
|
-
|
|
502
|
+
private postExposure;
|
|
380
503
|
/**
|
|
381
504
|
* Report a structured error into the errors primitive. Fire-and-forget —
|
|
382
505
|
* never blocks or throws into the request path. Spam-guarded by a 30s
|
|
@@ -460,12 +583,26 @@ interface ShipeasyServerConfig {
|
|
|
460
583
|
user?: User;
|
|
461
584
|
/** i18n profile to load for SSR translations, e.g. "en:prod". Defaults to "en:prod". */
|
|
462
585
|
i18nDefaultProfile?: string;
|
|
586
|
+
/**
|
|
587
|
+
* How chatty the SDK is on `console` when it swallows an internal error.
|
|
588
|
+
* `silent` < `error` < `warn` < `info` < `debug`; defaults to `"warn"`. Every
|
|
589
|
+
* runtime read/track/see call fails silently to a safe default and reports the
|
|
590
|
+
* cause at this level. See {@link EngineOptions.logLevel}.
|
|
591
|
+
*/
|
|
592
|
+
logLevel?: LogLevel;
|
|
463
593
|
/**
|
|
464
594
|
* Disable per-evaluation usage telemetry. ON by default. On Cloudflare
|
|
465
595
|
* Workers each beacon is an outbound subrequest, so disable on hot SSR paths
|
|
466
596
|
* that evaluate many flags per request. See {@link EngineOptions.disableTelemetry}.
|
|
467
597
|
*/
|
|
468
598
|
disableTelemetry?: boolean;
|
|
599
|
+
/**
|
|
600
|
+
* Opt out of SDK-internal error self-monitoring. Internal SDK failures ("on
|
|
601
|
+
* our end") are reported to Shipeasy's own project (never yours) so we can
|
|
602
|
+
* track SDK bugs. ON by default. See
|
|
603
|
+
* {@link EngineOptions.disableInternalErrorReporting}.
|
|
604
|
+
*/
|
|
605
|
+
disableInternalErrorReporting?: boolean;
|
|
469
606
|
/**
|
|
470
607
|
* Attribute names usable for targeting but never persisted in analytics
|
|
471
608
|
* (LD/Statsig `privateAttributes`). Stripped from every outbound `track()`
|
|
@@ -561,7 +698,17 @@ declare const flags: {
|
|
|
561
698
|
/** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
|
|
562
699
|
getDetail(name: string, user: User): FlagDetail;
|
|
563
700
|
getConfig<T = unknown>(name: string, decodeOrOpts?: ((raw: unknown) => T) | GetConfigOptions<T>): T | undefined;
|
|
564
|
-
|
|
701
|
+
/**
|
|
702
|
+
* Assign `user` within a universe: `flags.universe("checkout").assign(user)`.
|
|
703
|
+
* A universe is a mutual-exclusion pool, so the unit lands in ≤1 experiment;
|
|
704
|
+
* the returned {@link Assignment} exposes `.group` / `.get(field, fallback)`
|
|
705
|
+
* and auto-logs one exposure when enrolled. Before configure() (or on any
|
|
706
|
+
* error) it returns a safe not-enrolled handle. This replaces the removed
|
|
707
|
+
* `getExperiment` — read experiments by universe, never by name.
|
|
708
|
+
*/
|
|
709
|
+
universe(name: string): {
|
|
710
|
+
assign(user: User): Assignment;
|
|
711
|
+
};
|
|
565
712
|
/**
|
|
566
713
|
* Read a killswitch. Without `switchKey`, returns true when the whole
|
|
567
714
|
* killswitch is killed. With `switchKey`, returns true when that specific
|
|
@@ -569,9 +716,6 @@ declare const flags: {
|
|
|
569
716
|
*/
|
|
570
717
|
ks(name: string, switchKey?: string): boolean;
|
|
571
718
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
572
|
-
/** Emit an exposure for an enrolled experiment at the decision point. See
|
|
573
|
-
* {@link Engine.logExposure}. No-op before configure(). */
|
|
574
|
-
logExposure(user: string | User, name: string): void;
|
|
575
719
|
/**
|
|
576
720
|
* Evaluate all flags / configs / experiments for a user against the locally
|
|
577
721
|
* cached blob. Pass the request URL to apply ?se_ks_* / ?se_cf_* / ?se_exp_*
|
|
@@ -719,7 +863,15 @@ declare class Client<U = unknown> {
|
|
|
719
863
|
getFlagDetail(name: string): FlagDetail;
|
|
720
864
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
721
865
|
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
722
|
-
|
|
866
|
+
/**
|
|
867
|
+
* Assign the bound user within a universe: `client.universe("checkout").assign()`.
|
|
868
|
+
* The user is already bound at construction, so `assign()` takes no arg. Returns
|
|
869
|
+
* an {@link Assignment} (`.group` / `.get(field, fallback)`) and auto-logs one
|
|
870
|
+
* exposure when enrolled. Replaces the removed `getExperiment`.
|
|
871
|
+
*/
|
|
872
|
+
universe(name: string): {
|
|
873
|
+
assign(): Assignment;
|
|
874
|
+
};
|
|
723
875
|
/** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
|
|
724
876
|
getKillswitch(name: string, switchKey?: string): boolean;
|
|
725
877
|
/**
|
|
@@ -730,13 +882,6 @@ declare class Client<U = unknown> {
|
|
|
730
882
|
* mode.
|
|
731
883
|
*/
|
|
732
884
|
track(eventName: string, props?: Record<string, unknown>): void;
|
|
733
|
-
/**
|
|
734
|
-
* Emit an exposure event for `name` at this server-side decision point for the
|
|
735
|
-
* bound user. Delegates to {@link Engine.logExposure} with the resolved
|
|
736
|
-
* attribute bag (re-evaluates enrolment; no-op when the user isn't enrolled or
|
|
737
|
-
* in test mode).
|
|
738
|
-
*/
|
|
739
|
-
logExposure(name: string): void;
|
|
740
885
|
}
|
|
741
886
|
interface SeeApi {
|
|
742
887
|
/**
|
|
@@ -789,4 +934,4 @@ interface SeeApi {
|
|
|
789
934
|
*/
|
|
790
935
|
declare const see: SeeApi;
|
|
791
936
|
|
|
792
|
-
export { ANON_ID_COOKIE, type AttributesFn, type BootstrapData, type BootstrapEmitOptions, type BootstrapPayload, Client, type ConfigureOfflineOptions, type ConfigureOptions, type ConfigureTestOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, type ExpsBlob, FLAG_REASONS, type FetchLabelsOptions, type FlagDetail, type FlagReason, type FlagsBlob, type GetConfigOptions, type I18nForRequest, type LabelFile, type ScriptTagSpec, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type StickyBucketStore, type StickyEntry, type User, type Violation, _murmur3ForTests, _resetConfigureForTests, _resetShipeasyServerForTests, clearOverrides, configure, configureForOffline, configureForTesting, configureShipeasyServer, createInMemoryStickyStore, fetchLabelsForSSR, flags, getBootstrapData, getBootstrapTags, getShipeasyServerClient, i18n, isExpected, onChange, overrideConfig, overrideExperiment, overrideFlag, see, seeContext, shipeasy, version };
|
|
937
|
+
export { ANON_ID_COOKIE, type Assignment, type AttributesFn, type BootstrapData, type BootstrapEmitOptions, type BootstrapPayload, Client, type ConfigureOfflineOptions, type ConfigureOptions, type ConfigureTestOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, type ExpsBlob, FLAG_REASONS, type FetchLabelsOptions, type FlagDetail, type FlagReason, type FlagsBlob, type GetConfigOptions, type I18nForRequest, LOG_LEVELS, type LabelFile, type LogLevel, type ScriptTagSpec, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type StickyBucketStore, type StickyEntry, type UniverseParam, type UniverseParamSchema, type User, type Violation, _murmur3ForTests, _resetConfigureForTests, _resetShipeasyServerForTests, clearOverrides, configure, configureForOffline, configureForTesting, configureShipeasyServer, createInMemoryStickyStore, fetchLabelsForSSR, flags, getBootstrapData, getBootstrapTags, getShipeasyServerClient, i18n, isExpected, onChange, overrideConfig, overrideExperiment, overrideFlag, see, seeContext, shipeasy, version };
|