@shipeasy/sdk 5.4.0 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -313
- package/dist/client/index.d.mts +102 -16
- package/dist/client/index.d.ts +102 -16
- package/dist/client/index.js +81 -8
- package/dist/client/index.mjs +77 -7
- package/dist/openfeature-server/index.d.mts +33 -20
- package/dist/openfeature-server/index.d.ts +33 -20
- package/dist/openfeature-server/index.js +194 -4
- package/dist/openfeature-server/index.mjs +194 -4
- package/dist/openfeature-web/index.d.mts +12 -12
- package/dist/openfeature-web/index.d.ts +12 -12
- package/dist/server/index.d.mts +180 -21
- package/dist/server/index.d.ts +180 -21
- package/dist/server/index.js +159 -12
- package/dist/server/index.mjs +148 -11
- package/dist/skill-cli.js +56 -0
- package/docs/skill/SKILL.md +145 -0
- package/package.json +7 -1
package/dist/client/index.d.ts
CHANGED
|
@@ -202,8 +202,8 @@ declare function sameOrigin(rawUrl: string): boolean;
|
|
|
202
202
|
* pass through unchanged (correlation is optional, never breaks the fetch).
|
|
203
203
|
*/
|
|
204
204
|
declare function injectCorrelationHeader(args: Parameters<typeof fetch>, corr: string): Parameters<typeof fetch>;
|
|
205
|
-
type
|
|
206
|
-
interface
|
|
205
|
+
type EngineEnv = "dev" | "staging" | "prod";
|
|
206
|
+
interface EngineOptions {
|
|
207
207
|
sdkKey: string;
|
|
208
208
|
baseUrl?: string;
|
|
209
209
|
autoGuardrails?: boolean;
|
|
@@ -222,7 +222,7 @@ interface FlagsClientBrowserOptions {
|
|
|
222
222
|
*/
|
|
223
223
|
autoCollectAlways?: boolean;
|
|
224
224
|
/** Which published env to read values from. Defaults to "prod". */
|
|
225
|
-
env?:
|
|
225
|
+
env?: EngineEnv;
|
|
226
226
|
/**
|
|
227
227
|
* Per-evaluation usage telemetry. ON by default — each getFlag/getConfig/
|
|
228
228
|
* getExperiment/getKillswitch call fires one fire-and-forget sendBeacon so
|
|
@@ -259,11 +259,11 @@ interface FlagsClientBrowserOptions {
|
|
|
259
259
|
* Test mode — no network at all. identify()/init are no-ops (never call
|
|
260
260
|
* /sdk/evaluate), track() is a no-op, telemetry is forced off, and the client
|
|
261
261
|
* starts "ready" with an empty eval result. Prefer the
|
|
262
|
-
* {@link
|
|
262
|
+
* {@link Engine.forTesting} factory over passing this directly.
|
|
263
263
|
*/
|
|
264
264
|
testMode?: boolean;
|
|
265
265
|
}
|
|
266
|
-
declare class
|
|
266
|
+
declare class Engine {
|
|
267
267
|
private readonly sdkKey;
|
|
268
268
|
private readonly baseUrl;
|
|
269
269
|
private readonly autoGuardrails;
|
|
@@ -288,7 +288,7 @@ declare class FlagsClientBrowser {
|
|
|
288
288
|
private readonly configOverrides;
|
|
289
289
|
private readonly experimentOverrides;
|
|
290
290
|
private onOverrideChange;
|
|
291
|
-
constructor(opts:
|
|
291
|
+
constructor(opts: EngineOptions);
|
|
292
292
|
/**
|
|
293
293
|
* Build a no-network, immediately-usable browser client for tests
|
|
294
294
|
* (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
|
|
@@ -297,12 +297,12 @@ declare class FlagsClientBrowser {
|
|
|
297
297
|
* key required.
|
|
298
298
|
*
|
|
299
299
|
* ```ts
|
|
300
|
-
* const client =
|
|
300
|
+
* const client = Engine.forTesting();
|
|
301
301
|
* client.overrideFlag("new_checkout", true);
|
|
302
302
|
* client.getFlag("new_checkout"); // true
|
|
303
303
|
* ```
|
|
304
304
|
*/
|
|
305
|
-
static forTesting(opts?: Partial<
|
|
305
|
+
static forTesting(opts?: Partial<EngineOptions>): Engine;
|
|
306
306
|
identify(user: User): Promise<void>;
|
|
307
307
|
/**
|
|
308
308
|
* Report a structured error into the errors primitive. Flushes immediately
|
|
@@ -411,7 +411,7 @@ interface AttachDevtoolsOptions {
|
|
|
411
411
|
* each `identify()`/override change. Returns an unsubscribe function for
|
|
412
412
|
* cleanup (e.g. React effect teardown).
|
|
413
413
|
*/
|
|
414
|
-
declare function attachDevtools(client:
|
|
414
|
+
declare function attachDevtools(client: Engine, opts?: AttachDevtoolsOptions): () => void;
|
|
415
415
|
/** Configure the singleton. Idempotent — re-calling with the same opts is a no-op. */
|
|
416
416
|
interface ShipeasyClientConfig {
|
|
417
417
|
/**
|
|
@@ -484,13 +484,13 @@ interface ShipeasyClientConfig {
|
|
|
484
484
|
* Attribute names usable for targeting but never persisted in analytics
|
|
485
485
|
* (LD/Statsig `privateAttributes`). Sent to the edge for evaluation, never
|
|
486
486
|
* stored, and stripped from `flags.track(props)`. See
|
|
487
|
-
* {@link
|
|
487
|
+
* {@link EngineOptions.privateAttributes}.
|
|
488
488
|
*/
|
|
489
489
|
privateAttributes?: string[];
|
|
490
490
|
/**
|
|
491
491
|
* Sticky bucketing (doc 20 §2). ON by default — locks each enrolled unit to
|
|
492
492
|
* its first-assigned variant via the `__se_sticky` cookie. Pass `false` to
|
|
493
|
-
* opt out. See {@link
|
|
493
|
+
* opt out. See {@link EngineOptions.stickyBucketing}.
|
|
494
494
|
*/
|
|
495
495
|
stickyBucketing?: boolean;
|
|
496
496
|
}
|
|
@@ -505,9 +505,9 @@ interface ShipeasyClientConfig {
|
|
|
505
505
|
* A later flags.identify({ user_id }) overrides this in place; anonId stays stable.
|
|
506
506
|
*/
|
|
507
507
|
declare function shipeasy(opts: ShipeasyClientConfig): () => void;
|
|
508
|
-
declare function configureShipeasy(opts:
|
|
508
|
+
declare function configureShipeasy(opts: EngineOptions): Engine;
|
|
509
509
|
/** Returns the configured singleton, or null if configureShipeasy() hasn't run yet. */
|
|
510
|
-
declare function getShipeasyClient():
|
|
510
|
+
declare function getShipeasyClient(): Engine | null;
|
|
511
511
|
/**
|
|
512
512
|
* Test helper — drop the singleton so the next configureShipeasy() builds fresh.
|
|
513
513
|
* Not part of the documented surface; production code should never call this.
|
|
@@ -539,7 +539,7 @@ interface BootstrapPayload {
|
|
|
539
539
|
* importing this in a module that loads before app boot is harmless.
|
|
540
540
|
*/
|
|
541
541
|
declare const flags: {
|
|
542
|
-
configure(opts:
|
|
542
|
+
configure(opts: EngineOptions): void;
|
|
543
543
|
identify(user: User): Promise<void>;
|
|
544
544
|
/**
|
|
545
545
|
* Read a feature gate.
|
|
@@ -554,7 +554,7 @@ declare const flags: {
|
|
|
554
554
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
555
555
|
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decodeOrOpts?: ((raw: unknown) => P) | GetExperimentOptions<P>, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
|
|
556
556
|
/** Manually log an exposure for an enrolled experiment. See
|
|
557
|
-
* {@link
|
|
557
|
+
* {@link Engine.logExposure}. No-op before configure(). */
|
|
558
558
|
logExposure(name: string): void;
|
|
559
559
|
track(eventName: string, props?: Record<string, unknown>): void;
|
|
560
560
|
/**
|
|
@@ -583,6 +583,92 @@ declare const flags: {
|
|
|
583
583
|
/** True once identify() has completed and flags are available. */
|
|
584
584
|
readonly ready: boolean;
|
|
585
585
|
};
|
|
586
|
+
/** Transform YOUR application's user object into Shipeasy targeting attributes. */
|
|
587
|
+
type AttributesFn<U = unknown> = (user: U) => User;
|
|
588
|
+
interface ConfigureOptions<U = unknown> extends Omit<ShipeasyClientConfig, "clientKey"> {
|
|
589
|
+
/** Public client key — the single key the browser side accepts (NEXT_PUBLIC_SHIPEASY_CLIENT_KEY). */
|
|
590
|
+
clientKey: string;
|
|
591
|
+
/**
|
|
592
|
+
* Map your own user object into the attribute bag every flag/experiment
|
|
593
|
+
* evaluation sees. Runs once per `new Client(user)`. Omit when you already
|
|
594
|
+
* pass a plain attribute object (identity transform — the object is used
|
|
595
|
+
* verbatim, so it should carry `user_id` + any targeting attrs).
|
|
596
|
+
*/
|
|
597
|
+
attributes?: AttributesFn<U>;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Configure the SDK once at app boot, then evaluate per user with
|
|
601
|
+
* `new Client(user)`. Builds the process-wide {@link Engine} (the
|
|
602
|
+
* /sdk/evaluate-backed browser client) and registers the `attributes`
|
|
603
|
+
* transform. The first call wins; later calls reuse the existing engine
|
|
604
|
+
* (mirrors {@link shipeasy}).
|
|
605
|
+
*
|
|
606
|
+
* ```ts
|
|
607
|
+
* import { configure, Client } from "@shipeasy/sdk/client";
|
|
608
|
+
*
|
|
609
|
+
* configure({
|
|
610
|
+
* clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY!,
|
|
611
|
+
* attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan }),
|
|
612
|
+
* });
|
|
613
|
+
*
|
|
614
|
+
* const flags = new Client(currentUser);
|
|
615
|
+
* await flags.ready();
|
|
616
|
+
* if (flags.getFlag("new_checkout")) { ... }
|
|
617
|
+
* ```
|
|
618
|
+
*
|
|
619
|
+
* Returns a cleanup function (same as {@link shipeasy}) that removes the
|
|
620
|
+
* devtools listeners — call it on teardown.
|
|
621
|
+
*/
|
|
622
|
+
declare function configure<U = unknown>(opts: ConfigureOptions<U>): () => void;
|
|
623
|
+
/** Test seam: reset the registered attribute transform. */
|
|
624
|
+
declare function _resetConfigureForTests(): void;
|
|
625
|
+
/**
|
|
626
|
+
* A user-bound evaluation handle for the browser. Construct one for the current
|
|
627
|
+
* visitor — it's cheap (it delegates to the single {@link Engine} built by
|
|
628
|
+
* {@link configure}); it does NOT open its own connection. The configured
|
|
629
|
+
* `attributes` transform runs once here and the result is `identify()`-ed into
|
|
630
|
+
* the engine (fire-and-forget, since identify is async).
|
|
631
|
+
*
|
|
632
|
+
* Because the browser is single-user, all bound handles share the engine's
|
|
633
|
+
* latest eval result. Use {@link Client.ready} to await the first evaluation.
|
|
634
|
+
*
|
|
635
|
+
* ```ts
|
|
636
|
+
* const flags = new Client(currentUser);
|
|
637
|
+
* await flags.ready();
|
|
638
|
+
* flags.getFlag("new_checkout"); // no user arg — bound at construction
|
|
639
|
+
* flags.getExperiment("price_test", { price: 9 });
|
|
640
|
+
* ```
|
|
641
|
+
*/
|
|
642
|
+
declare class Client<U = unknown> {
|
|
643
|
+
private readonly engine;
|
|
644
|
+
/** The resolved attribute bag this handle evaluates against. */
|
|
645
|
+
readonly attributes: User;
|
|
646
|
+
private readonly _identify;
|
|
647
|
+
constructor(user: U);
|
|
648
|
+
/** Resolves once the engine's identify() for this user has completed. */
|
|
649
|
+
ready(): Promise<void>;
|
|
650
|
+
getFlag(name: string, defaultValue?: boolean): boolean;
|
|
651
|
+
getFlagDetail(name: string): FlagDetail;
|
|
652
|
+
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
653
|
+
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
654
|
+
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
655
|
+
/** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
|
|
656
|
+
getKillswitch(name: string, switchKey?: string): boolean;
|
|
657
|
+
/**
|
|
658
|
+
* Record a conversion/metric event for the bound (identified) user. Delegates
|
|
659
|
+
* to {@link Engine.track} — so an experiment is end-to-end Client-only (no need
|
|
660
|
+
* to drop down to the Engine to log a conversion). Fire-and-forget; no-op in
|
|
661
|
+
* test mode.
|
|
662
|
+
*/
|
|
663
|
+
track(eventName: string, props?: Record<string, unknown>): void;
|
|
664
|
+
/**
|
|
665
|
+
* Log an exposure for `name` at the treatment's render for the bound user.
|
|
666
|
+
* Delegates to {@link Engine.logExposure} (no-op when the visitor isn't
|
|
667
|
+
* enrolled). Pair with `getExperiment(name, …, { logExposure: false })` /
|
|
668
|
+
* `disableAutoExposure` to log exposure exactly when you render.
|
|
669
|
+
*/
|
|
670
|
+
logExposure(name: string): void;
|
|
671
|
+
}
|
|
586
672
|
interface SeeApi {
|
|
587
673
|
/**
|
|
588
674
|
* Report a handled problem and its product consequence:
|
|
@@ -674,4 +760,4 @@ interface I18nFacade {
|
|
|
674
760
|
}
|
|
675
761
|
declare const i18n: I18nFacade;
|
|
676
762
|
|
|
677
|
-
export { type AutoCollectGroups, type BootstrapPayload, type
|
|
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 };
|
package/dist/client/index.js
CHANGED
|
@@ -20,14 +20,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/client/index.ts
|
|
21
21
|
var client_exports = {};
|
|
22
22
|
__export(client_exports, {
|
|
23
|
+
Client: () => Client,
|
|
24
|
+
Engine: () => Engine,
|
|
23
25
|
FLAG_REASONS: () => FLAG_REASONS,
|
|
24
|
-
FlagsClientBrowser: () => FlagsClientBrowser,
|
|
25
26
|
LABEL_MARKER_END: () => LABEL_MARKER_END,
|
|
26
27
|
LABEL_MARKER_RE: () => LABEL_MARKER_RE,
|
|
27
28
|
LABEL_MARKER_SEP: () => LABEL_MARKER_SEP,
|
|
28
29
|
LABEL_MARKER_START: () => LABEL_MARKER_START,
|
|
30
|
+
_resetConfigureForTests: () => _resetConfigureForTests,
|
|
29
31
|
_resetShipeasyForTests: () => _resetShipeasyForTests,
|
|
30
32
|
attachDevtools: () => attachDevtools,
|
|
33
|
+
configure: () => configure,
|
|
31
34
|
configureShipeasy: () => configureShipeasy,
|
|
32
35
|
encodeLabelMarker: () => encodeLabelMarker,
|
|
33
36
|
flags: () => flags,
|
|
@@ -912,7 +915,7 @@ function readExperimentOverridesFromUrl() {
|
|
|
912
915
|
}
|
|
913
916
|
return out;
|
|
914
917
|
}
|
|
915
|
-
var
|
|
918
|
+
var Engine = class _Engine {
|
|
916
919
|
sdkKey;
|
|
917
920
|
baseUrl;
|
|
918
921
|
autoGuardrails;
|
|
@@ -934,7 +937,7 @@ var FlagsClientBrowser = class _FlagsClientBrowser {
|
|
|
934
937
|
// Monotonic counter so a later identify() always wins even if its /sdk/evaluate
|
|
935
938
|
// response races and lands before an earlier in-flight call's response.
|
|
936
939
|
identifySeq = 0;
|
|
937
|
-
// Test mode: built by `
|
|
940
|
+
// Test mode: built by `Engine.forTesting()`. When set, identify()
|
|
938
941
|
// never fetches, track() is a no-op, telemetry is off, and the client is
|
|
939
942
|
// already ready with an empty eval result.
|
|
940
943
|
testMode;
|
|
@@ -988,13 +991,13 @@ var FlagsClientBrowser = class _FlagsClientBrowser {
|
|
|
988
991
|
* key required.
|
|
989
992
|
*
|
|
990
993
|
* ```ts
|
|
991
|
-
* const client =
|
|
994
|
+
* const client = Engine.forTesting();
|
|
992
995
|
* client.overrideFlag("new_checkout", true);
|
|
993
996
|
* client.getFlag("new_checkout"); // true
|
|
994
997
|
* ```
|
|
995
998
|
*/
|
|
996
999
|
static forTesting(opts) {
|
|
997
|
-
return new
|
|
1000
|
+
return new _Engine({
|
|
998
1001
|
sdkKey: "",
|
|
999
1002
|
autoGuardrails: false,
|
|
1000
1003
|
...opts,
|
|
@@ -1415,7 +1418,7 @@ function shipeasy(opts) {
|
|
|
1415
1418
|
}
|
|
1416
1419
|
function configureShipeasy(opts) {
|
|
1417
1420
|
if (_client) return _client;
|
|
1418
|
-
_client = new
|
|
1421
|
+
_client = new Engine(opts);
|
|
1419
1422
|
return _client;
|
|
1420
1423
|
}
|
|
1421
1424
|
function getShipeasyClient() {
|
|
@@ -1554,7 +1557,7 @@ var flags = {
|
|
|
1554
1557
|
return typeof decodeOrOpts === "function" ? _client.getExperiment(name, defaultParams, decodeOrOpts, variants) : _client.getExperiment(name, defaultParams, decodeOrOpts ?? {});
|
|
1555
1558
|
},
|
|
1556
1559
|
/** Manually log an exposure for an enrolled experiment. See
|
|
1557
|
-
* {@link
|
|
1560
|
+
* {@link Engine.logExposure}. No-op before configure(). */
|
|
1558
1561
|
logExposure(name) {
|
|
1559
1562
|
_client?.logExposure(name);
|
|
1560
1563
|
},
|
|
@@ -1611,6 +1614,73 @@ var flags = {
|
|
|
1611
1614
|
return _client?.ready ?? false;
|
|
1612
1615
|
}
|
|
1613
1616
|
};
|
|
1617
|
+
var _identityAttributes = (user) => user && typeof user === "object" ? user : {};
|
|
1618
|
+
var _attributes = _identityAttributes;
|
|
1619
|
+
function configure(opts) {
|
|
1620
|
+
const { attributes, ...clientConfig } = opts;
|
|
1621
|
+
_attributes = attributes ?? _identityAttributes;
|
|
1622
|
+
return shipeasy({ autoIdentify: false, ...clientConfig });
|
|
1623
|
+
}
|
|
1624
|
+
function _resetConfigureForTests() {
|
|
1625
|
+
_attributes = _identityAttributes;
|
|
1626
|
+
}
|
|
1627
|
+
var Client = class {
|
|
1628
|
+
engine;
|
|
1629
|
+
/** The resolved attribute bag this handle evaluates against. */
|
|
1630
|
+
attributes;
|
|
1631
|
+
_identify;
|
|
1632
|
+
constructor(user) {
|
|
1633
|
+
const engine = getShipeasyClient();
|
|
1634
|
+
if (!engine) {
|
|
1635
|
+
throw new Error(
|
|
1636
|
+
"[shipeasy] new Client(user) called before configure({ clientKey }). Call configure() once at app boot from @shipeasy/sdk/client."
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
this.engine = engine;
|
|
1640
|
+
this.attributes = _attributes(user);
|
|
1641
|
+
this._identify = this.engine.identify(this.attributes).catch((err) => {
|
|
1642
|
+
console.warn("[shipeasy] Client identify failed:", String(err));
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
/** Resolves once the engine's identify() for this user has completed. */
|
|
1646
|
+
ready() {
|
|
1647
|
+
return this._identify;
|
|
1648
|
+
}
|
|
1649
|
+
getFlag(name, defaultValue = false) {
|
|
1650
|
+
return this.engine.getFlag(name, defaultValue);
|
|
1651
|
+
}
|
|
1652
|
+
getFlagDetail(name) {
|
|
1653
|
+
return this.engine.getFlagDetail(name);
|
|
1654
|
+
}
|
|
1655
|
+
getConfig(name, decodeOrOpts) {
|
|
1656
|
+
return this.engine.getConfig(name, decodeOrOpts);
|
|
1657
|
+
}
|
|
1658
|
+
getExperiment(name, defaultParams, decode) {
|
|
1659
|
+
return this.engine.getExperiment(name, defaultParams, decode);
|
|
1660
|
+
}
|
|
1661
|
+
/** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
|
|
1662
|
+
getKillswitch(name, switchKey) {
|
|
1663
|
+
return this.engine.getKillswitch(name, switchKey);
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Record a conversion/metric event for the bound (identified) user. Delegates
|
|
1667
|
+
* to {@link Engine.track} — so an experiment is end-to-end Client-only (no need
|
|
1668
|
+
* to drop down to the Engine to log a conversion). Fire-and-forget; no-op in
|
|
1669
|
+
* test mode.
|
|
1670
|
+
*/
|
|
1671
|
+
track(eventName, props) {
|
|
1672
|
+
this.engine.track(eventName, props);
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* Log an exposure for `name` at the treatment's render for the bound user.
|
|
1676
|
+
* Delegates to {@link Engine.logExposure} (no-op when the visitor isn't
|
|
1677
|
+
* enrolled). Pair with `getExperiment(name, …, { logExposure: false })` /
|
|
1678
|
+
* `disableAutoExposure` to log exposure exactly when you render.
|
|
1679
|
+
*/
|
|
1680
|
+
logExposure(name) {
|
|
1681
|
+
this.engine.logExposure(name);
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1614
1684
|
function dispatchSee(problem, consequence, extras, kind) {
|
|
1615
1685
|
if (!_client) {
|
|
1616
1686
|
console.warn("[shipeasy] see() called before shipeasy({ clientKey }) \u2014 error dropped");
|
|
@@ -1859,14 +1929,17 @@ var i18n = {
|
|
|
1859
1929
|
};
|
|
1860
1930
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1861
1931
|
0 && (module.exports = {
|
|
1932
|
+
Client,
|
|
1933
|
+
Engine,
|
|
1862
1934
|
FLAG_REASONS,
|
|
1863
|
-
FlagsClientBrowser,
|
|
1864
1935
|
LABEL_MARKER_END,
|
|
1865
1936
|
LABEL_MARKER_RE,
|
|
1866
1937
|
LABEL_MARKER_SEP,
|
|
1867
1938
|
LABEL_MARKER_START,
|
|
1939
|
+
_resetConfigureForTests,
|
|
1868
1940
|
_resetShipeasyForTests,
|
|
1869
1941
|
attachDevtools,
|
|
1942
|
+
configure,
|
|
1870
1943
|
configureShipeasy,
|
|
1871
1944
|
encodeLabelMarker,
|
|
1872
1945
|
flags,
|
package/dist/client/index.mjs
CHANGED
|
@@ -863,7 +863,7 @@ function readExperimentOverridesFromUrl() {
|
|
|
863
863
|
}
|
|
864
864
|
return out;
|
|
865
865
|
}
|
|
866
|
-
var
|
|
866
|
+
var Engine = class _Engine {
|
|
867
867
|
sdkKey;
|
|
868
868
|
baseUrl;
|
|
869
869
|
autoGuardrails;
|
|
@@ -885,7 +885,7 @@ var FlagsClientBrowser = class _FlagsClientBrowser {
|
|
|
885
885
|
// Monotonic counter so a later identify() always wins even if its /sdk/evaluate
|
|
886
886
|
// response races and lands before an earlier in-flight call's response.
|
|
887
887
|
identifySeq = 0;
|
|
888
|
-
// Test mode: built by `
|
|
888
|
+
// Test mode: built by `Engine.forTesting()`. When set, identify()
|
|
889
889
|
// never fetches, track() is a no-op, telemetry is off, and the client is
|
|
890
890
|
// already ready with an empty eval result.
|
|
891
891
|
testMode;
|
|
@@ -939,13 +939,13 @@ var FlagsClientBrowser = class _FlagsClientBrowser {
|
|
|
939
939
|
* key required.
|
|
940
940
|
*
|
|
941
941
|
* ```ts
|
|
942
|
-
* const client =
|
|
942
|
+
* const client = Engine.forTesting();
|
|
943
943
|
* client.overrideFlag("new_checkout", true);
|
|
944
944
|
* client.getFlag("new_checkout"); // true
|
|
945
945
|
* ```
|
|
946
946
|
*/
|
|
947
947
|
static forTesting(opts) {
|
|
948
|
-
return new
|
|
948
|
+
return new _Engine({
|
|
949
949
|
sdkKey: "",
|
|
950
950
|
autoGuardrails: false,
|
|
951
951
|
...opts,
|
|
@@ -1366,7 +1366,7 @@ function shipeasy(opts) {
|
|
|
1366
1366
|
}
|
|
1367
1367
|
function configureShipeasy(opts) {
|
|
1368
1368
|
if (_client) return _client;
|
|
1369
|
-
_client = new
|
|
1369
|
+
_client = new Engine(opts);
|
|
1370
1370
|
return _client;
|
|
1371
1371
|
}
|
|
1372
1372
|
function getShipeasyClient() {
|
|
@@ -1505,7 +1505,7 @@ var flags = {
|
|
|
1505
1505
|
return typeof decodeOrOpts === "function" ? _client.getExperiment(name, defaultParams, decodeOrOpts, variants) : _client.getExperiment(name, defaultParams, decodeOrOpts ?? {});
|
|
1506
1506
|
},
|
|
1507
1507
|
/** Manually log an exposure for an enrolled experiment. See
|
|
1508
|
-
* {@link
|
|
1508
|
+
* {@link Engine.logExposure}. No-op before configure(). */
|
|
1509
1509
|
logExposure(name) {
|
|
1510
1510
|
_client?.logExposure(name);
|
|
1511
1511
|
},
|
|
@@ -1562,6 +1562,73 @@ var flags = {
|
|
|
1562
1562
|
return _client?.ready ?? false;
|
|
1563
1563
|
}
|
|
1564
1564
|
};
|
|
1565
|
+
var _identityAttributes = (user) => user && typeof user === "object" ? user : {};
|
|
1566
|
+
var _attributes = _identityAttributes;
|
|
1567
|
+
function configure(opts) {
|
|
1568
|
+
const { attributes, ...clientConfig } = opts;
|
|
1569
|
+
_attributes = attributes ?? _identityAttributes;
|
|
1570
|
+
return shipeasy({ autoIdentify: false, ...clientConfig });
|
|
1571
|
+
}
|
|
1572
|
+
function _resetConfigureForTests() {
|
|
1573
|
+
_attributes = _identityAttributes;
|
|
1574
|
+
}
|
|
1575
|
+
var Client = class {
|
|
1576
|
+
engine;
|
|
1577
|
+
/** The resolved attribute bag this handle evaluates against. */
|
|
1578
|
+
attributes;
|
|
1579
|
+
_identify;
|
|
1580
|
+
constructor(user) {
|
|
1581
|
+
const engine = getShipeasyClient();
|
|
1582
|
+
if (!engine) {
|
|
1583
|
+
throw new Error(
|
|
1584
|
+
"[shipeasy] new Client(user) called before configure({ clientKey }). Call configure() once at app boot from @shipeasy/sdk/client."
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1587
|
+
this.engine = engine;
|
|
1588
|
+
this.attributes = _attributes(user);
|
|
1589
|
+
this._identify = this.engine.identify(this.attributes).catch((err) => {
|
|
1590
|
+
console.warn("[shipeasy] Client identify failed:", String(err));
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
/** Resolves once the engine's identify() for this user has completed. */
|
|
1594
|
+
ready() {
|
|
1595
|
+
return this._identify;
|
|
1596
|
+
}
|
|
1597
|
+
getFlag(name, defaultValue = false) {
|
|
1598
|
+
return this.engine.getFlag(name, defaultValue);
|
|
1599
|
+
}
|
|
1600
|
+
getFlagDetail(name) {
|
|
1601
|
+
return this.engine.getFlagDetail(name);
|
|
1602
|
+
}
|
|
1603
|
+
getConfig(name, decodeOrOpts) {
|
|
1604
|
+
return this.engine.getConfig(name, decodeOrOpts);
|
|
1605
|
+
}
|
|
1606
|
+
getExperiment(name, defaultParams, decode) {
|
|
1607
|
+
return this.engine.getExperiment(name, defaultParams, decode);
|
|
1608
|
+
}
|
|
1609
|
+
/** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
|
|
1610
|
+
getKillswitch(name, switchKey) {
|
|
1611
|
+
return this.engine.getKillswitch(name, switchKey);
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Record a conversion/metric event for the bound (identified) user. Delegates
|
|
1615
|
+
* to {@link Engine.track} — so an experiment is end-to-end Client-only (no need
|
|
1616
|
+
* to drop down to the Engine to log a conversion). Fire-and-forget; no-op in
|
|
1617
|
+
* test mode.
|
|
1618
|
+
*/
|
|
1619
|
+
track(eventName, props) {
|
|
1620
|
+
this.engine.track(eventName, props);
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Log an exposure for `name` at the treatment's render for the bound user.
|
|
1624
|
+
* Delegates to {@link Engine.logExposure} (no-op when the visitor isn't
|
|
1625
|
+
* enrolled). Pair with `getExperiment(name, …, { logExposure: false })` /
|
|
1626
|
+
* `disableAutoExposure` to log exposure exactly when you render.
|
|
1627
|
+
*/
|
|
1628
|
+
logExposure(name) {
|
|
1629
|
+
this.engine.logExposure(name);
|
|
1630
|
+
}
|
|
1631
|
+
};
|
|
1565
1632
|
function dispatchSee(problem, consequence, extras, kind) {
|
|
1566
1633
|
if (!_client) {
|
|
1567
1634
|
console.warn("[shipeasy] see() called before shipeasy({ clientKey }) \u2014 error dropped");
|
|
@@ -1809,14 +1876,17 @@ var i18n = {
|
|
|
1809
1876
|
}
|
|
1810
1877
|
};
|
|
1811
1878
|
export {
|
|
1879
|
+
Client,
|
|
1880
|
+
Engine,
|
|
1812
1881
|
FLAG_REASONS,
|
|
1813
|
-
FlagsClientBrowser,
|
|
1814
1882
|
LABEL_MARKER_END,
|
|
1815
1883
|
LABEL_MARKER_RE,
|
|
1816
1884
|
LABEL_MARKER_SEP,
|
|
1817
1885
|
LABEL_MARKER_START,
|
|
1886
|
+
_resetConfigureForTests,
|
|
1818
1887
|
_resetShipeasyForTests,
|
|
1819
1888
|
attachDevtools,
|
|
1889
|
+
configure,
|
|
1820
1890
|
configureShipeasy,
|
|
1821
1891
|
encodeLabelMarker,
|
|
1822
1892
|
flags,
|
|
@@ -78,7 +78,7 @@ interface StickyEntry {
|
|
|
78
78
|
/**
|
|
79
79
|
* Pluggable sticky-bucketing store for the server (doc 20 §2). Keyed by the
|
|
80
80
|
* bucketing unit; the value is that unit's per-experiment assignments. Absent
|
|
81
|
-
* from {@link
|
|
81
|
+
* from {@link EngineOptions} ⇒ today's deterministic behaviour. Use
|
|
82
82
|
* {@link createInMemoryStickyStore} or a cookie-bridge built from request
|
|
83
83
|
* cookies.
|
|
84
84
|
*/
|
|
@@ -93,7 +93,7 @@ interface Killswitch {
|
|
|
93
93
|
killed: 0 | 1 | boolean;
|
|
94
94
|
switches?: Record<string, 0 | 1 | boolean>;
|
|
95
95
|
}
|
|
96
|
-
/** Body of `GET /sdk/flags` — the snapshot's `flags` field. See {@link
|
|
96
|
+
/** Body of `GET /sdk/flags` — the snapshot's `flags` field. See {@link Engine.fromSnapshot}. */
|
|
97
97
|
interface FlagsBlob {
|
|
98
98
|
version: string;
|
|
99
99
|
plan: string;
|
|
@@ -103,7 +103,7 @@ interface FlagsBlob {
|
|
|
103
103
|
}>;
|
|
104
104
|
killswitches: Record<string, Killswitch>;
|
|
105
105
|
}
|
|
106
|
-
/** Body of `GET /sdk/experiments` — the snapshot's `experiments` field. See {@link
|
|
106
|
+
/** Body of `GET /sdk/experiments` — the snapshot's `experiments` field. See {@link Engine.fromSnapshot}. */
|
|
107
107
|
interface ExpsBlob {
|
|
108
108
|
version: string;
|
|
109
109
|
universes: Record<string, Universe>;
|
|
@@ -115,12 +115,12 @@ interface BootstrapPayload {
|
|
|
115
115
|
experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
|
|
116
116
|
killswitches: Record<string, boolean | Record<string, boolean>>;
|
|
117
117
|
}
|
|
118
|
-
type
|
|
119
|
-
interface
|
|
118
|
+
type EngineEnv = "dev" | "staging" | "prod";
|
|
119
|
+
interface EngineOptions {
|
|
120
120
|
apiKey: string;
|
|
121
121
|
baseUrl?: string;
|
|
122
122
|
/** Which published env to read values from. Defaults to "prod". */
|
|
123
|
-
env?:
|
|
123
|
+
env?: EngineEnv;
|
|
124
124
|
/**
|
|
125
125
|
* Preload the flags blob synchronously without a network fetch. Primarily
|
|
126
126
|
* for tests; production callers should rely on init()/initOnce().
|
|
@@ -155,12 +155,12 @@ interface FlagsClientOptions {
|
|
|
155
155
|
/**
|
|
156
156
|
* Test mode — no network at all. init()/initOnce() are no-ops (never fetch),
|
|
157
157
|
* track() is a no-op, telemetry is forced off, and the client starts
|
|
158
|
-
* "initialized" with an empty blob. Prefer the {@link
|
|
158
|
+
* "initialized" with an empty blob. Prefer the {@link Engine.forTesting}
|
|
159
159
|
* factory over passing this directly.
|
|
160
160
|
*/
|
|
161
161
|
testMode?: boolean;
|
|
162
162
|
}
|
|
163
|
-
declare class
|
|
163
|
+
declare class Engine {
|
|
164
164
|
private readonly apiKey;
|
|
165
165
|
private readonly baseUrl;
|
|
166
166
|
private readonly env;
|
|
@@ -180,7 +180,7 @@ declare class FlagsClient {
|
|
|
180
180
|
private readonly configOverrides;
|
|
181
181
|
private readonly experimentOverrides;
|
|
182
182
|
private readonly changeListeners;
|
|
183
|
-
constructor(opts:
|
|
183
|
+
constructor(opts: EngineOptions);
|
|
184
184
|
/**
|
|
185
185
|
* Build a no-network, immediately-usable client for tests (Statsig-style).
|
|
186
186
|
* init()/initOnce() are no-ops (never fetch), track() is a no-op, telemetry
|
|
@@ -188,12 +188,12 @@ declare class FlagsClient {
|
|
|
188
188
|
* with overrideFlag/overrideConfig/overrideExperiment. No SDK key required.
|
|
189
189
|
*
|
|
190
190
|
* ```ts
|
|
191
|
-
* const client =
|
|
191
|
+
* const client = Engine.forTesting();
|
|
192
192
|
* client.overrideFlag("new_checkout", true);
|
|
193
193
|
* client.getFlag("new_checkout", { user_id: "u1" }); // true
|
|
194
194
|
* ```
|
|
195
195
|
*/
|
|
196
|
-
static forTesting(opts?: Partial<
|
|
196
|
+
static forTesting(opts?: Partial<EngineOptions>): Engine;
|
|
197
197
|
/**
|
|
198
198
|
* Build a fully OFFLINE client from a pre-captured snapshot — no network ever.
|
|
199
199
|
* Reuses the test-mode plumbing (init()/initOnce()/track() are no-ops,
|
|
@@ -207,14 +207,14 @@ declare class FlagsClient {
|
|
|
207
207
|
static fromSnapshot(snapshot: {
|
|
208
208
|
flags: FlagsBlob;
|
|
209
209
|
experiments: ExpsBlob;
|
|
210
|
-
}):
|
|
210
|
+
}): Engine;
|
|
211
211
|
/**
|
|
212
212
|
* Build a fully OFFLINE client from a snapshot JSON file on disk (Node only —
|
|
213
213
|
* not available in the browser entrypoint). The file must contain
|
|
214
214
|
* `{ "flags": <GET /sdk/flags body>, "experiments": <GET /sdk/experiments body> }`.
|
|
215
|
-
* See {@link
|
|
215
|
+
* See {@link Engine.fromSnapshot}.
|
|
216
216
|
*/
|
|
217
|
-
static fromFile(path: string):
|
|
217
|
+
static fromFile(path: string): Engine;
|
|
218
218
|
init(): Promise<void>;
|
|
219
219
|
initOnce(): Promise<void>;
|
|
220
220
|
/** Force `getFlag(name, …)` to return `value`, ignoring the fetched gate. */
|
|
@@ -298,32 +298,45 @@ declare class FlagsClient {
|
|
|
298
298
|
*
|
|
299
299
|
* ```ts
|
|
300
300
|
* import { OpenFeature } from "@openfeature/server-sdk";
|
|
301
|
-
* import {
|
|
301
|
+
* import { Engine } from "@shipeasy/sdk/server";
|
|
302
302
|
* import { ShipeasyProvider } from "@shipeasy/sdk/openfeature-server";
|
|
303
303
|
*
|
|
304
|
-
* const client = new
|
|
304
|
+
* const client = new Engine({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
|
|
305
305
|
* await OpenFeature.setProviderAndWait(new ShipeasyProvider(client));
|
|
306
306
|
*
|
|
307
307
|
* const ofClient = OpenFeature.getClient();
|
|
308
308
|
* const on = await ofClient.getBooleanValue("new_checkout", false, { targetingKey: "u1" });
|
|
309
309
|
* ```
|
|
310
310
|
*
|
|
311
|
-
* Pure adapter over `
|
|
311
|
+
* Pure adapter over `Engine` — no change to evaluation. `@openfeature/server-sdk`
|
|
312
312
|
* is an optional peer dependency; install it in the consuming app.
|
|
313
313
|
*/
|
|
314
314
|
|
|
315
315
|
/**
|
|
316
|
-
* Shipeasy OpenFeature provider (server paradigm). Wraps
|
|
316
|
+
* Shipeasy OpenFeature provider (server paradigm). Wraps an `Engine`;
|
|
317
317
|
* evaluation is local against the cached blob, so resolution is effectively
|
|
318
318
|
* synchronous (the methods are `async` only to satisfy the server contract).
|
|
319
319
|
*/
|
|
320
320
|
declare class ShipeasyProvider implements Provider {
|
|
321
|
-
private readonly client;
|
|
322
321
|
readonly metadata: {
|
|
323
322
|
readonly name: "shipeasy";
|
|
324
323
|
};
|
|
325
324
|
readonly runsOn: "server";
|
|
326
|
-
|
|
325
|
+
private readonly client;
|
|
326
|
+
/**
|
|
327
|
+
* Construct the provider. The **global form** (no argument) resolves the
|
|
328
|
+
* engine built by `configure({ apiKey })`, so the docs build it after
|
|
329
|
+
* configuration without ever naming the `Engine`:
|
|
330
|
+
*
|
|
331
|
+
* ```ts
|
|
332
|
+
* configure({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
|
|
333
|
+
* await OpenFeature.setProviderAndWait(new ShipeasyProvider());
|
|
334
|
+
* ```
|
|
335
|
+
*
|
|
336
|
+
* Passing an explicit `Engine` stays supported for advanced/back-compat use.
|
|
337
|
+
* Throws if no engine is passed and `configure()` has not run.
|
|
338
|
+
*/
|
|
339
|
+
constructor(client?: Engine);
|
|
327
340
|
/** Fetch the rules blob once. The SDK fires `Ready` when this resolves. */
|
|
328
341
|
initialize(): Promise<void>;
|
|
329
342
|
onClose(): Promise<void>;
|