@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/server/index.d.ts
CHANGED
|
@@ -181,7 +181,7 @@ interface StickyEntry {
|
|
|
181
181
|
/**
|
|
182
182
|
* Pluggable sticky-bucketing store for the server (doc 20 §2). Keyed by the
|
|
183
183
|
* bucketing unit; the value is that unit's per-experiment assignments. Absent
|
|
184
|
-
* from {@link
|
|
184
|
+
* from {@link EngineOptions} ⇒ today's deterministic behaviour. Use
|
|
185
185
|
* {@link createInMemoryStickyStore} or a cookie-bridge built from request
|
|
186
186
|
* cookies.
|
|
187
187
|
*/
|
|
@@ -198,7 +198,7 @@ interface Killswitch {
|
|
|
198
198
|
killed: 0 | 1 | boolean;
|
|
199
199
|
switches?: Record<string, 0 | 1 | boolean>;
|
|
200
200
|
}
|
|
201
|
-
/** Body of `GET /sdk/flags` — the snapshot's `flags` field. See {@link
|
|
201
|
+
/** Body of `GET /sdk/flags` — the snapshot's `flags` field. See {@link Engine.fromSnapshot}. */
|
|
202
202
|
interface FlagsBlob {
|
|
203
203
|
version: string;
|
|
204
204
|
plan: string;
|
|
@@ -208,7 +208,7 @@ interface FlagsBlob {
|
|
|
208
208
|
}>;
|
|
209
209
|
killswitches: Record<string, Killswitch>;
|
|
210
210
|
}
|
|
211
|
-
/** Body of `GET /sdk/experiments` — the snapshot's `experiments` field. See {@link
|
|
211
|
+
/** Body of `GET /sdk/experiments` — the snapshot's `experiments` field. See {@link Engine.fromSnapshot}. */
|
|
212
212
|
interface ExpsBlob {
|
|
213
213
|
version: string;
|
|
214
214
|
universes: Record<string, Universe>;
|
|
@@ -221,12 +221,12 @@ interface BootstrapPayload {
|
|
|
221
221
|
killswitches: Record<string, boolean | Record<string, boolean>>;
|
|
222
222
|
}
|
|
223
223
|
declare const ANON_ID_COOKIE = "__se_anon_id";
|
|
224
|
-
type
|
|
225
|
-
interface
|
|
224
|
+
type EngineEnv = "dev" | "staging" | "prod";
|
|
225
|
+
interface EngineOptions {
|
|
226
226
|
apiKey: string;
|
|
227
227
|
baseUrl?: string;
|
|
228
228
|
/** Which published env to read values from. Defaults to "prod". */
|
|
229
|
-
env?:
|
|
229
|
+
env?: EngineEnv;
|
|
230
230
|
/**
|
|
231
231
|
* Preload the flags blob synchronously without a network fetch. Primarily
|
|
232
232
|
* for tests; production callers should rely on init()/initOnce().
|
|
@@ -261,12 +261,12 @@ interface FlagsClientOptions {
|
|
|
261
261
|
/**
|
|
262
262
|
* Test mode — no network at all. init()/initOnce() are no-ops (never fetch),
|
|
263
263
|
* track() is a no-op, telemetry is forced off, and the client starts
|
|
264
|
-
* "initialized" with an empty blob. Prefer the {@link
|
|
264
|
+
* "initialized" with an empty blob. Prefer the {@link Engine.forTesting}
|
|
265
265
|
* factory over passing this directly.
|
|
266
266
|
*/
|
|
267
267
|
testMode?: boolean;
|
|
268
268
|
}
|
|
269
|
-
declare class
|
|
269
|
+
declare class Engine {
|
|
270
270
|
private readonly apiKey;
|
|
271
271
|
private readonly baseUrl;
|
|
272
272
|
private readonly env;
|
|
@@ -286,7 +286,7 @@ declare class FlagsClient {
|
|
|
286
286
|
private readonly configOverrides;
|
|
287
287
|
private readonly experimentOverrides;
|
|
288
288
|
private readonly changeListeners;
|
|
289
|
-
constructor(opts:
|
|
289
|
+
constructor(opts: EngineOptions);
|
|
290
290
|
/**
|
|
291
291
|
* Build a no-network, immediately-usable client for tests (Statsig-style).
|
|
292
292
|
* init()/initOnce() are no-ops (never fetch), track() is a no-op, telemetry
|
|
@@ -294,12 +294,12 @@ declare class FlagsClient {
|
|
|
294
294
|
* with overrideFlag/overrideConfig/overrideExperiment. No SDK key required.
|
|
295
295
|
*
|
|
296
296
|
* ```ts
|
|
297
|
-
* const client =
|
|
297
|
+
* const client = Engine.forTesting();
|
|
298
298
|
* client.overrideFlag("new_checkout", true);
|
|
299
299
|
* client.getFlag("new_checkout", { user_id: "u1" }); // true
|
|
300
300
|
* ```
|
|
301
301
|
*/
|
|
302
|
-
static forTesting(opts?: Partial<
|
|
302
|
+
static forTesting(opts?: Partial<EngineOptions>): Engine;
|
|
303
303
|
/**
|
|
304
304
|
* Build a fully OFFLINE client from a pre-captured snapshot — no network ever.
|
|
305
305
|
* Reuses the test-mode plumbing (init()/initOnce()/track() are no-ops,
|
|
@@ -313,14 +313,14 @@ declare class FlagsClient {
|
|
|
313
313
|
static fromSnapshot(snapshot: {
|
|
314
314
|
flags: FlagsBlob;
|
|
315
315
|
experiments: ExpsBlob;
|
|
316
|
-
}):
|
|
316
|
+
}): Engine;
|
|
317
317
|
/**
|
|
318
318
|
* Build a fully OFFLINE client from a snapshot JSON file on disk (Node only —
|
|
319
319
|
* not available in the browser entrypoint). The file must contain
|
|
320
320
|
* `{ "flags": <GET /sdk/flags body>, "experiments": <GET /sdk/experiments body> }`.
|
|
321
|
-
* See {@link
|
|
321
|
+
* See {@link Engine.fromSnapshot}.
|
|
322
322
|
*/
|
|
323
|
-
static fromFile(path: string):
|
|
323
|
+
static fromFile(path: string): Engine;
|
|
324
324
|
init(): Promise<void>;
|
|
325
325
|
initOnce(): Promise<void>;
|
|
326
326
|
/** Force `getFlag(name, …)` to return `value`, ignoring the fetched gate. */
|
|
@@ -440,8 +440,8 @@ interface FetchLabelsOptions {
|
|
|
440
440
|
timeoutMs?: number;
|
|
441
441
|
}
|
|
442
442
|
declare function fetchLabelsForSSR(opts: FetchLabelsOptions): Promise<LabelFile | null>;
|
|
443
|
-
declare function configureShipeasyServer(opts:
|
|
444
|
-
declare function getShipeasyServerClient():
|
|
443
|
+
declare function configureShipeasyServer(opts: EngineOptions): Engine;
|
|
444
|
+
declare function getShipeasyServerClient(): Engine | null;
|
|
445
445
|
declare function _resetShipeasyServerForTests(): void;
|
|
446
446
|
interface ShipeasyServerConfig {
|
|
447
447
|
/**
|
|
@@ -463,13 +463,13 @@ interface ShipeasyServerConfig {
|
|
|
463
463
|
/**
|
|
464
464
|
* Disable per-evaluation usage telemetry. ON by default. On Cloudflare
|
|
465
465
|
* Workers each beacon is an outbound subrequest, so disable on hot SSR paths
|
|
466
|
-
* that evaluate many flags per request. See {@link
|
|
466
|
+
* that evaluate many flags per request. See {@link EngineOptions.disableTelemetry}.
|
|
467
467
|
*/
|
|
468
468
|
disableTelemetry?: boolean;
|
|
469
469
|
/**
|
|
470
470
|
* Attribute names usable for targeting but never persisted in analytics
|
|
471
471
|
* (LD/Statsig `privateAttributes`). Stripped from every outbound `track()`
|
|
472
|
-
* payload. See {@link
|
|
472
|
+
* payload. See {@link EngineOptions.privateAttributes}.
|
|
473
473
|
*/
|
|
474
474
|
privateAttributes?: string[];
|
|
475
475
|
}
|
|
@@ -547,7 +547,7 @@ declare function getBootstrapData(bootstrap: BootstrapPayload | null, i18nData:
|
|
|
547
547
|
*/
|
|
548
548
|
declare function getBootstrapTags(bootstrap: BootstrapPayload | null, i18nData: I18nForRequest | null, opts: BootstrapEmitOptions): string;
|
|
549
549
|
declare const flags: {
|
|
550
|
-
configure(opts:
|
|
550
|
+
configure(opts: EngineOptions): void;
|
|
551
551
|
/**
|
|
552
552
|
* Long-running server: starts the background poll. Call once at app boot.
|
|
553
553
|
* Throws if the initial fetch fails (caller decides whether to crash or degrade).
|
|
@@ -570,7 +570,7 @@ declare const flags: {
|
|
|
570
570
|
ks(name: string, switchKey?: string): boolean;
|
|
571
571
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
572
572
|
/** Emit an exposure for an enrolled experiment at the decision point. See
|
|
573
|
-
* {@link
|
|
573
|
+
* {@link Engine.logExposure}. No-op before configure(). */
|
|
574
574
|
logExposure(user: string | User, name: string): void;
|
|
575
575
|
/**
|
|
576
576
|
* Evaluate all flags / configs / experiments for a user against the locally
|
|
@@ -579,6 +579,165 @@ declare const flags: {
|
|
|
579
579
|
*/
|
|
580
580
|
evaluate(user: User, rawUrl?: string): BootstrapPayload;
|
|
581
581
|
};
|
|
582
|
+
/** Transform YOUR application's user object into Shipeasy targeting attributes. */
|
|
583
|
+
type AttributesFn<U = unknown> = (user: U) => User;
|
|
584
|
+
interface ConfigureOptions<U = unknown> extends Omit<EngineOptions, "apiKey"> {
|
|
585
|
+
/** Server key — the single key the server side accepts (SHIPEASY_SERVER_KEY). */
|
|
586
|
+
apiKey: string;
|
|
587
|
+
/**
|
|
588
|
+
* Map your own user object into the attribute bag every flag/experiment
|
|
589
|
+
* evaluation sees. Runs once per `new Client(user)`. Omit when you already
|
|
590
|
+
* pass a plain attribute object (identity transform — the object is used
|
|
591
|
+
* verbatim, so it should carry `user_id`/`anonymous_id` + any targeting attrs).
|
|
592
|
+
*/
|
|
593
|
+
attributes?: AttributesFn<U>;
|
|
594
|
+
/**
|
|
595
|
+
* Long-running server: start the **background poll** internally (initial fetch
|
|
596
|
+
* + periodic refresh) so flags stay fresh without a redeploy. Default `false`.
|
|
597
|
+
* With `poll: false` (default) a one-shot fetch is kicked off fire-and-forget
|
|
598
|
+
* (serverless-friendly). You never need to call `engine.init()` yourself —
|
|
599
|
+
* configuration owns the lifecycle.
|
|
600
|
+
*/
|
|
601
|
+
poll?: boolean;
|
|
602
|
+
/**
|
|
603
|
+
* One-shot fetch on configure (fire-and-forget). Default `true`. Ignored when
|
|
604
|
+
* `poll: true` (the poll does the initial fetch). Set `false` only if you want
|
|
605
|
+
* to control the first fetch yourself.
|
|
606
|
+
*/
|
|
607
|
+
init?: boolean;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Configure the SDK once at app boot, then evaluate per user with
|
|
611
|
+
* `new Client(user)`. Builds the process-wide {@link Engine} (polling + blob
|
|
612
|
+
* cache + HTTP) and registers the `attributes` transform. The first call wins;
|
|
613
|
+
* later calls return the existing engine (mirrors {@link configureShipeasyServer}).
|
|
614
|
+
*
|
|
615
|
+
* ```ts
|
|
616
|
+
* import { configure, Client } from "@shipeasy/sdk/server";
|
|
617
|
+
*
|
|
618
|
+
* configure({
|
|
619
|
+
* apiKey: process.env.SHIPEASY_SERVER_KEY!,
|
|
620
|
+
* attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan, country: u.geo.country }),
|
|
621
|
+
* });
|
|
622
|
+
*
|
|
623
|
+
* const flags = new Client(currentUser);
|
|
624
|
+
* if (flags.getFlag("new_checkout")) { ... }
|
|
625
|
+
* ```
|
|
626
|
+
*/
|
|
627
|
+
declare function configure<U = unknown>(opts: ConfigureOptions<U>): Engine;
|
|
628
|
+
/** Test seam: reset the registered attribute transform. */
|
|
629
|
+
declare function _resetConfigureForTests(): void;
|
|
630
|
+
/** Seed shapes shared by {@link configureForTesting} / {@link configureForOffline}. */
|
|
631
|
+
interface ConfigureTestOptions<U = unknown> {
|
|
632
|
+
/** Same transform as {@link configure} (default identity). */
|
|
633
|
+
attributes?: AttributesFn<U>;
|
|
634
|
+
/** `{ name: bool }` — forced `getFlag` results. */
|
|
635
|
+
flags?: Record<string, boolean>;
|
|
636
|
+
/** `{ name: value }` — forced `getConfig` results. */
|
|
637
|
+
configs?: Record<string, unknown>;
|
|
638
|
+
/** `{ name: [group, params] }` — forced enrolments. */
|
|
639
|
+
experiments?: Record<string, [string, Record<string, unknown>]>;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Configure the SDK in **test mode** — a drop-in sibling of {@link configure}
|
|
643
|
+
* with no network, ever (no api key needed). Seed what your code under test
|
|
644
|
+
* should see, then read through the ordinary `new Client(user)`:
|
|
645
|
+
*
|
|
646
|
+
* ```ts
|
|
647
|
+
* configureForTesting({ flags: { new_checkout: true } });
|
|
648
|
+
* const flags = new Client({ user_id: "u_1" });
|
|
649
|
+
* flags.getFlag("new_checkout"); // true
|
|
650
|
+
* ```
|
|
651
|
+
*
|
|
652
|
+
* Replaces any previously-configured engine, so tests can reconfigure freely.
|
|
653
|
+
*/
|
|
654
|
+
declare function configureForTesting<U = unknown>(opts?: ConfigureTestOptions<U>): Engine;
|
|
655
|
+
/** Options for {@link configureForOffline} — exactly one of `snapshot` / `path`. */
|
|
656
|
+
interface ConfigureOfflineOptions<U = unknown> extends ConfigureTestOptions<U> {
|
|
657
|
+
/** In-memory `{ flags: <body of /sdk/flags>, experiments: <body of /sdk/experiments> }`. */
|
|
658
|
+
snapshot?: {
|
|
659
|
+
flags: FlagsBlob;
|
|
660
|
+
experiments: ExpsBlob;
|
|
661
|
+
};
|
|
662
|
+
/** Path to a JSON file `{ flags, experiments }`. */
|
|
663
|
+
path?: string;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Configure the SDK **offline** — evaluate the REAL rules from an in-memory
|
|
667
|
+
* snapshot or a JSON file, with no network. A drop-in sibling of
|
|
668
|
+
* {@link configure} (no api key needed). Optional `flags`/`configs`/`experiments`
|
|
669
|
+
* overrides are layered on top (same shapes as {@link configureForTesting}).
|
|
670
|
+
* Replaces any previously-configured engine.
|
|
671
|
+
*/
|
|
672
|
+
declare function configureForOffline<U = unknown>(opts: ConfigureOfflineOptions<U>): Engine;
|
|
673
|
+
/**
|
|
674
|
+
* Force `getFlag(name)` → `value` on the spot, for the current config. A quick
|
|
675
|
+
* in-test override layered on top of whatever {@link configureForTesting} /
|
|
676
|
+
* {@link configureForOffline} (or {@link configure}) set up — wins over the blob
|
|
677
|
+
* until {@link clearOverrides}.
|
|
678
|
+
*/
|
|
679
|
+
declare function overrideFlag(name: string, value: boolean): void;
|
|
680
|
+
/** Force `getConfig(name)` → `value` on the spot (see {@link overrideFlag}). */
|
|
681
|
+
declare function overrideConfig(name: string, value: unknown): void;
|
|
682
|
+
/**
|
|
683
|
+
* Force `getExperiment(name)` to report enrolment in `group` with `params` on
|
|
684
|
+
* the spot (see {@link overrideFlag}).
|
|
685
|
+
*/
|
|
686
|
+
declare function overrideExperiment(name: string, group: string, params: Record<string, unknown>): void;
|
|
687
|
+
/**
|
|
688
|
+
* Drop every on-the-spot flag/config/experiment override — INCLUDING the seed
|
|
689
|
+
* from {@link configureForTesting} (test mode has no blob beneath, so everything
|
|
690
|
+
* reverts to the empty-blob defaults). Under {@link configureForOffline} the
|
|
691
|
+
* snapshot remains and evaluations revert to it.
|
|
692
|
+
*/
|
|
693
|
+
declare function clearOverrides(): void;
|
|
694
|
+
/**
|
|
695
|
+
* Register a listener fired after a background poll fetches NEW data (a 200, not
|
|
696
|
+
* a 304). Returns an unsubscribe callable. Requires `configure({ poll: true })`
|
|
697
|
+
* (no poll thread runs otherwise). Configuration owns the engine; you never
|
|
698
|
+
* touch it.
|
|
699
|
+
*/
|
|
700
|
+
declare function onChange(listener: () => void): () => void;
|
|
701
|
+
/**
|
|
702
|
+
* A user-bound evaluation handle. Construct one per user/request — it's cheap
|
|
703
|
+
* (it delegates to the {@link Engine} built by {@link configure}); it does NOT
|
|
704
|
+
* open its own connection or poll. The configured `attributes` transform runs
|
|
705
|
+
* once here, so every getFlag/getConfig/getExperiment reads the same bag.
|
|
706
|
+
*
|
|
707
|
+
* ```ts
|
|
708
|
+
* const flags = new Client(req.user);
|
|
709
|
+
* flags.getFlag("new_checkout"); // no user arg — bound at construction
|
|
710
|
+
* flags.getExperiment("price_test", { price: 9 });
|
|
711
|
+
* ```
|
|
712
|
+
*/
|
|
713
|
+
declare class Client<U = unknown> {
|
|
714
|
+
private readonly engine;
|
|
715
|
+
/** The resolved attribute bag this handle evaluates against. */
|
|
716
|
+
readonly attributes: User;
|
|
717
|
+
constructor(user: U);
|
|
718
|
+
getFlag(name: string, defaultValue?: boolean): boolean;
|
|
719
|
+
getFlagDetail(name: string): FlagDetail;
|
|
720
|
+
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
721
|
+
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
722
|
+
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
723
|
+
/** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
|
|
724
|
+
getKillswitch(name: string, switchKey?: string): boolean;
|
|
725
|
+
/**
|
|
726
|
+
* Record a conversion/metric event for the bound user. Derives the unit from
|
|
727
|
+
* the resolved attribute bag (`user_id`, else `anonymous_id`) and delegates to
|
|
728
|
+
* {@link Engine.track} — so an experiment is end-to-end Client-only (no need to
|
|
729
|
+
* drop down to the Engine to log a conversion). Fire-and-forget; no-op in test
|
|
730
|
+
* mode.
|
|
731
|
+
*/
|
|
732
|
+
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
|
+
}
|
|
582
741
|
interface SeeApi {
|
|
583
742
|
/**
|
|
584
743
|
* Report a handled problem and its product consequence:
|
|
@@ -630,4 +789,4 @@ interface SeeApi {
|
|
|
630
789
|
*/
|
|
631
790
|
declare const see: SeeApi;
|
|
632
791
|
|
|
633
|
-
export { ANON_ID_COOKIE, type BootstrapData, type BootstrapEmitOptions, type BootstrapPayload, type
|
|
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 };
|
package/dist/server/index.js
CHANGED
|
@@ -31,10 +31,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var server_exports = {};
|
|
32
32
|
__export(server_exports, {
|
|
33
33
|
ANON_ID_COOKIE: () => ANON_ID_COOKIE,
|
|
34
|
+
Client: () => Client,
|
|
35
|
+
Engine: () => Engine,
|
|
34
36
|
FLAG_REASONS: () => FLAG_REASONS,
|
|
35
|
-
FlagsClient: () => FlagsClient,
|
|
36
37
|
_murmur3ForTests: () => _murmur3ForTests,
|
|
38
|
+
_resetConfigureForTests: () => _resetConfigureForTests,
|
|
37
39
|
_resetShipeasyServerForTests: () => _resetShipeasyServerForTests,
|
|
40
|
+
clearOverrides: () => clearOverrides,
|
|
41
|
+
configure: () => configure,
|
|
42
|
+
configureForOffline: () => configureForOffline,
|
|
43
|
+
configureForTesting: () => configureForTesting,
|
|
38
44
|
configureShipeasyServer: () => configureShipeasyServer,
|
|
39
45
|
createInMemoryStickyStore: () => createInMemoryStickyStore,
|
|
40
46
|
fetchLabelsForSSR: () => fetchLabelsForSSR,
|
|
@@ -44,6 +50,10 @@ __export(server_exports, {
|
|
|
44
50
|
getShipeasyServerClient: () => getShipeasyServerClient,
|
|
45
51
|
i18n: () => i18n,
|
|
46
52
|
isExpected: () => isExpected,
|
|
53
|
+
onChange: () => onChange,
|
|
54
|
+
overrideConfig: () => overrideConfig,
|
|
55
|
+
overrideExperiment: () => overrideExperiment,
|
|
56
|
+
overrideFlag: () => overrideFlag,
|
|
47
57
|
see: () => see,
|
|
48
58
|
seeContext: () => seeContext,
|
|
49
59
|
shipeasy: () => shipeasy,
|
|
@@ -543,7 +553,7 @@ function parseOverrides(rawUrl) {
|
|
|
543
553
|
}
|
|
544
554
|
return { gates, configs, experiments };
|
|
545
555
|
}
|
|
546
|
-
var
|
|
556
|
+
var Engine = class _Engine {
|
|
547
557
|
apiKey;
|
|
548
558
|
baseUrl;
|
|
549
559
|
env;
|
|
@@ -558,7 +568,7 @@ var FlagsClient = class _FlagsClient {
|
|
|
558
568
|
pollInterval = 30;
|
|
559
569
|
timer = null;
|
|
560
570
|
initialized = false;
|
|
561
|
-
// Test mode: built by `
|
|
571
|
+
// Test mode: built by `Engine.forTesting()`. When set, init()/initOnce()
|
|
562
572
|
// never fetch, track() is a no-op, and telemetry is off — the client is a
|
|
563
573
|
// fully self-contained, network-free seam for unit tests.
|
|
564
574
|
testMode;
|
|
@@ -599,13 +609,13 @@ var FlagsClient = class _FlagsClient {
|
|
|
599
609
|
* with overrideFlag/overrideConfig/overrideExperiment. No SDK key required.
|
|
600
610
|
*
|
|
601
611
|
* ```ts
|
|
602
|
-
* const client =
|
|
612
|
+
* const client = Engine.forTesting();
|
|
603
613
|
* client.overrideFlag("new_checkout", true);
|
|
604
614
|
* client.getFlag("new_checkout", { user_id: "u1" }); // true
|
|
605
615
|
* ```
|
|
606
616
|
*/
|
|
607
617
|
static forTesting(opts) {
|
|
608
|
-
return new
|
|
618
|
+
return new _Engine({ apiKey: "", ...opts, testMode: true });
|
|
609
619
|
}
|
|
610
620
|
/**
|
|
611
621
|
* Build a fully OFFLINE client from a pre-captured snapshot — no network ever.
|
|
@@ -618,7 +628,7 @@ var FlagsClient = class _FlagsClient {
|
|
|
618
628
|
* `{ flags: <GET /sdk/flags body>, experiments: <GET /sdk/experiments body> }`.
|
|
619
629
|
*/
|
|
620
630
|
static fromSnapshot(snapshot) {
|
|
621
|
-
const client = new
|
|
631
|
+
const client = new _Engine({ apiKey: "", testMode: true });
|
|
622
632
|
client.flagsBlob = snapshot.flags;
|
|
623
633
|
client.expsBlob = snapshot.experiments;
|
|
624
634
|
client.initialized = true;
|
|
@@ -628,13 +638,13 @@ var FlagsClient = class _FlagsClient {
|
|
|
628
638
|
* Build a fully OFFLINE client from a snapshot JSON file on disk (Node only —
|
|
629
639
|
* not available in the browser entrypoint). The file must contain
|
|
630
640
|
* `{ "flags": <GET /sdk/flags body>, "experiments": <GET /sdk/experiments body> }`.
|
|
631
|
-
* See {@link
|
|
641
|
+
* See {@link Engine.fromSnapshot}.
|
|
632
642
|
*/
|
|
633
643
|
static fromFile(path) {
|
|
634
644
|
const fs = require("fs");
|
|
635
645
|
const raw = fs.readFileSync(path, "utf8");
|
|
636
646
|
const snapshot = JSON.parse(raw);
|
|
637
|
-
return
|
|
647
|
+
return _Engine.fromSnapshot(snapshot);
|
|
638
648
|
}
|
|
639
649
|
async init() {
|
|
640
650
|
if (this.testMode) {
|
|
@@ -978,7 +988,11 @@ var FlagsClient = class _FlagsClient {
|
|
|
978
988
|
const ks = this.flagsBlob?.killswitches?.[name];
|
|
979
989
|
if (!ks) return false;
|
|
980
990
|
if (switchKey === void 0) return isEnabled(ks.killed);
|
|
981
|
-
|
|
991
|
+
const switches = ks.switches ?? {};
|
|
992
|
+
if (Object.prototype.hasOwnProperty.call(switches, switchKey)) {
|
|
993
|
+
return isEnabled(switches[switchKey]);
|
|
994
|
+
}
|
|
995
|
+
return isEnabled(ks.killed);
|
|
982
996
|
}
|
|
983
997
|
};
|
|
984
998
|
var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
|
|
@@ -1094,7 +1108,7 @@ async function fetchLabelsForSSR(opts) {
|
|
|
1094
1108
|
var _server = null;
|
|
1095
1109
|
function configureShipeasyServer(opts) {
|
|
1096
1110
|
if (_server) return _server;
|
|
1097
|
-
_server = new
|
|
1111
|
+
_server = new Engine(opts);
|
|
1098
1112
|
return _server;
|
|
1099
1113
|
}
|
|
1100
1114
|
function getShipeasyServerClient() {
|
|
@@ -1269,7 +1283,7 @@ var flags = {
|
|
|
1269
1283
|
_server?.track(userId, eventName, props);
|
|
1270
1284
|
},
|
|
1271
1285
|
/** Emit an exposure for an enrolled experiment at the decision point. See
|
|
1272
|
-
* {@link
|
|
1286
|
+
* {@link Engine.logExposure}. No-op before configure(). */
|
|
1273
1287
|
logExposure(user, name) {
|
|
1274
1288
|
_server?.logExposure(user, name);
|
|
1275
1289
|
},
|
|
@@ -1287,6 +1301,129 @@ var flags = {
|
|
|
1287
1301
|
};
|
|
1288
1302
|
}
|
|
1289
1303
|
};
|
|
1304
|
+
var _identityAttributes = (user) => user && typeof user === "object" ? user : {};
|
|
1305
|
+
var _attributes = _identityAttributes;
|
|
1306
|
+
function configure(opts) {
|
|
1307
|
+
const { attributes, poll = false, init = true, ...engineOpts } = opts;
|
|
1308
|
+
_attributes = attributes ?? _identityAttributes;
|
|
1309
|
+
const engine = configureShipeasyServer(engineOpts);
|
|
1310
|
+
if (poll) {
|
|
1311
|
+
void engine.init().catch(() => {
|
|
1312
|
+
});
|
|
1313
|
+
} else if (init) {
|
|
1314
|
+
void engine.initOnce().catch(() => {
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
return engine;
|
|
1318
|
+
}
|
|
1319
|
+
function _resetConfigureForTests() {
|
|
1320
|
+
_attributes = _identityAttributes;
|
|
1321
|
+
}
|
|
1322
|
+
function _installGlobalEngine(engine, attributes) {
|
|
1323
|
+
_server?.destroy();
|
|
1324
|
+
_server = engine;
|
|
1325
|
+
_attributes = attributes ?? _identityAttributes;
|
|
1326
|
+
return engine;
|
|
1327
|
+
}
|
|
1328
|
+
function _applyOverrides(engine, flags2, configs, experiments) {
|
|
1329
|
+
for (const [name, value] of Object.entries(flags2 ?? {})) engine.overrideFlag(name, value);
|
|
1330
|
+
for (const [name, value] of Object.entries(configs ?? {})) engine.overrideConfig(name, value);
|
|
1331
|
+
for (const [name, [group, params]] of Object.entries(experiments ?? {}))
|
|
1332
|
+
engine.overrideExperiment(name, group, params);
|
|
1333
|
+
}
|
|
1334
|
+
function configureForTesting(opts = {}) {
|
|
1335
|
+
const engine = Engine.forTesting();
|
|
1336
|
+
_applyOverrides(engine, opts.flags, opts.configs, opts.experiments);
|
|
1337
|
+
return _installGlobalEngine(engine, opts.attributes);
|
|
1338
|
+
}
|
|
1339
|
+
function configureForOffline(opts) {
|
|
1340
|
+
let engine;
|
|
1341
|
+
if (opts.path !== void 0) {
|
|
1342
|
+
engine = Engine.fromFile(opts.path);
|
|
1343
|
+
} else if (opts.snapshot !== void 0) {
|
|
1344
|
+
engine = Engine.fromSnapshot(opts.snapshot);
|
|
1345
|
+
} else {
|
|
1346
|
+
throw new Error("[shipeasy] configureForOffline requires either { snapshot } or { path }");
|
|
1347
|
+
}
|
|
1348
|
+
_applyOverrides(engine, opts.flags, opts.configs, opts.experiments);
|
|
1349
|
+
return _installGlobalEngine(engine, opts.attributes);
|
|
1350
|
+
}
|
|
1351
|
+
function _requireGlobal(fn) {
|
|
1352
|
+
const engine = getShipeasyServerClient();
|
|
1353
|
+
if (!engine) {
|
|
1354
|
+
throw new Error(
|
|
1355
|
+
`[shipeasy] ${fn}(...) called before configure({ apiKey }) (or a configureFor* sibling).`
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
return engine;
|
|
1359
|
+
}
|
|
1360
|
+
function overrideFlag(name, value) {
|
|
1361
|
+
_requireGlobal("overrideFlag").overrideFlag(name, value);
|
|
1362
|
+
}
|
|
1363
|
+
function overrideConfig(name, value) {
|
|
1364
|
+
_requireGlobal("overrideConfig").overrideConfig(name, value);
|
|
1365
|
+
}
|
|
1366
|
+
function overrideExperiment(name, group, params) {
|
|
1367
|
+
_requireGlobal("overrideExperiment").overrideExperiment(name, group, params);
|
|
1368
|
+
}
|
|
1369
|
+
function clearOverrides() {
|
|
1370
|
+
_requireGlobal("clearOverrides").clearOverrides();
|
|
1371
|
+
}
|
|
1372
|
+
function onChange(listener) {
|
|
1373
|
+
return _requireGlobal("onChange").onChange(listener);
|
|
1374
|
+
}
|
|
1375
|
+
var Client = class {
|
|
1376
|
+
engine;
|
|
1377
|
+
/** The resolved attribute bag this handle evaluates against. */
|
|
1378
|
+
attributes;
|
|
1379
|
+
constructor(user) {
|
|
1380
|
+
const engine = getShipeasyServerClient();
|
|
1381
|
+
if (!engine) {
|
|
1382
|
+
throw new Error(
|
|
1383
|
+
"[shipeasy] new Client(user) called before configure({ apiKey }). Call configure() once at app boot from @shipeasy/sdk/server."
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
this.engine = engine;
|
|
1387
|
+
this.attributes = _attributes(user);
|
|
1388
|
+
}
|
|
1389
|
+
getFlag(name, defaultValue = false) {
|
|
1390
|
+
return this.engine.getFlag(name, this.attributes, defaultValue);
|
|
1391
|
+
}
|
|
1392
|
+
getFlagDetail(name) {
|
|
1393
|
+
return this.engine.getFlagDetail(name, this.attributes);
|
|
1394
|
+
}
|
|
1395
|
+
getConfig(name, decodeOrOpts) {
|
|
1396
|
+
return this.engine.getConfig(name, decodeOrOpts);
|
|
1397
|
+
}
|
|
1398
|
+
getExperiment(name, defaultParams, decode) {
|
|
1399
|
+
return this.engine.getExperiment(name, this.attributes, defaultParams, decode);
|
|
1400
|
+
}
|
|
1401
|
+
/** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
|
|
1402
|
+
getKillswitch(name, switchKey) {
|
|
1403
|
+
return this.engine.getKillswitch(name, switchKey);
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Record a conversion/metric event for the bound user. Derives the unit from
|
|
1407
|
+
* the resolved attribute bag (`user_id`, else `anonymous_id`) and delegates to
|
|
1408
|
+
* {@link Engine.track} — so an experiment is end-to-end Client-only (no need to
|
|
1409
|
+
* drop down to the Engine to log a conversion). Fire-and-forget; no-op in test
|
|
1410
|
+
* mode.
|
|
1411
|
+
*/
|
|
1412
|
+
track(eventName, props) {
|
|
1413
|
+
const id = this.attributes.user_id ?? this.attributes.anonymous_id;
|
|
1414
|
+
if (id === void 0) return;
|
|
1415
|
+
this.engine.track(String(id), eventName, props);
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Emit an exposure event for `name` at this server-side decision point for the
|
|
1419
|
+
* bound user. Delegates to {@link Engine.logExposure} with the resolved
|
|
1420
|
+
* attribute bag (re-evaluates enrolment; no-op when the user isn't enrolled or
|
|
1421
|
+
* in test mode).
|
|
1422
|
+
*/
|
|
1423
|
+
logExposure(name) {
|
|
1424
|
+
this.engine.logExposure(this.attributes, name);
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1290
1427
|
function dispatchSee(problem, consequence, extras, kind) {
|
|
1291
1428
|
if (!_server) {
|
|
1292
1429
|
console.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
|
|
@@ -1304,10 +1441,16 @@ var see = Object.assign(
|
|
|
1304
1441
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1305
1442
|
0 && (module.exports = {
|
|
1306
1443
|
ANON_ID_COOKIE,
|
|
1444
|
+
Client,
|
|
1445
|
+
Engine,
|
|
1307
1446
|
FLAG_REASONS,
|
|
1308
|
-
FlagsClient,
|
|
1309
1447
|
_murmur3ForTests,
|
|
1448
|
+
_resetConfigureForTests,
|
|
1310
1449
|
_resetShipeasyServerForTests,
|
|
1450
|
+
clearOverrides,
|
|
1451
|
+
configure,
|
|
1452
|
+
configureForOffline,
|
|
1453
|
+
configureForTesting,
|
|
1311
1454
|
configureShipeasyServer,
|
|
1312
1455
|
createInMemoryStickyStore,
|
|
1313
1456
|
fetchLabelsForSSR,
|
|
@@ -1317,6 +1460,10 @@ var see = Object.assign(
|
|
|
1317
1460
|
getShipeasyServerClient,
|
|
1318
1461
|
i18n,
|
|
1319
1462
|
isExpected,
|
|
1463
|
+
onChange,
|
|
1464
|
+
overrideConfig,
|
|
1465
|
+
overrideExperiment,
|
|
1466
|
+
overrideFlag,
|
|
1320
1467
|
see,
|
|
1321
1468
|
seeContext,
|
|
1322
1469
|
shipeasy,
|