@shipeasy/sdk 5.3.0 → 6.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 +106 -27
- package/dist/client/index.d.mts +90 -18
- package/dist/client/index.d.ts +90 -18
- package/dist/client/index.js +100 -11
- package/dist/client/index.mjs +96 -10
- package/dist/openfeature-server/index.d.mts +19 -19
- package/dist/openfeature-server/index.d.ts +19 -19
- package/dist/openfeature-web/index.d.mts +12 -12
- package/dist/openfeature-web/index.d.ts +12 -12
- package/dist/server/index.d.mts +134 -45
- package/dist/server/index.d.ts +134 -45
- package/dist/server/index.js +113 -45
- package/dist/server/index.mjs +107 -43
- package/package.json +1 -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
|
}
|
|
@@ -477,8 +477,22 @@ interface ShipeasyServerHandle {
|
|
|
477
477
|
flags: Record<string, boolean>;
|
|
478
478
|
configs: Record<string, unknown>;
|
|
479
479
|
experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
|
|
480
|
-
/**
|
|
481
|
-
|
|
480
|
+
/**
|
|
481
|
+
* Structured `<script>` tag specs to drop into the document head: the
|
|
482
|
+
* cross-platform `se-bootstrap.js` tag (hydrates window.__SE_BOOTSTRAP +
|
|
483
|
+
* writes the anon cookie) and, when there are SSR strings or a client key,
|
|
484
|
+
* the i18n loader tag. Use this in React: scripts inserted via
|
|
485
|
+
* `dangerouslySetInnerHTML` do NOT execute, so render real `<script>`
|
|
486
|
+
* elements from these specs (see apps/ui root layout).
|
|
487
|
+
*/
|
|
488
|
+
getBootstrapData(emit?: BootstrapEmitOptions): BootstrapData;
|
|
489
|
+
/**
|
|
490
|
+
* The same tags rendered as an HTML string, for non-React SSR (Express, raw
|
|
491
|
+
* templates) where the markup is emitted directly into the served HTML (and
|
|
492
|
+
* therefore executes normally). This is the canonical cross-platform shape
|
|
493
|
+
* every server SDK mirrors.
|
|
494
|
+
*/
|
|
495
|
+
getBootstrapTags(emit?: BootstrapEmitOptions): string;
|
|
482
496
|
}
|
|
483
497
|
/**
|
|
484
498
|
* Initialise the ShipEasy server SDK, evaluate flags for this request, and
|
|
@@ -487,37 +501,53 @@ interface ShipeasyServerHandle {
|
|
|
487
501
|
* payloads and i18n falls back to hardcoded strings.
|
|
488
502
|
*/
|
|
489
503
|
declare function shipeasy(opts: ShipeasyServerConfig): Promise<ShipeasyServerHandle>;
|
|
490
|
-
interface
|
|
491
|
-
/** i18n profile recorded
|
|
504
|
+
interface BootstrapEmitOptions {
|
|
505
|
+
/** i18n profile recorded on both tags so the client loader matches SSR. Defaults to "en:prod". */
|
|
492
506
|
i18nProfile?: string;
|
|
493
|
-
/** When true, tEl() embeds label markers so the devtools can highlight them. */
|
|
494
|
-
editLabels?: boolean;
|
|
495
507
|
/**
|
|
496
|
-
* Stable anonymous bucketing id the server evaluated against. Emitted
|
|
497
|
-
*
|
|
498
|
-
* `__se_anon_id` cookie, so the
|
|
499
|
-
*
|
|
500
|
-
* middleware doesn't cover. See
|
|
508
|
+
* Stable anonymous bucketing id the server evaluated against. Emitted as
|
|
509
|
+
* `data-anon-id`; se-bootstrap.js exposes it on window.__SE_BOOTSTRAP and
|
|
510
|
+
* persists it (pre-paint) to the first-party `__se_anon_id` cookie, so the
|
|
511
|
+
* browser SDK buckets identically to SSR. Normally minted by edge
|
|
512
|
+
* middleware; this is the fallback for routes it doesn't cover. See
|
|
513
|
+
* experiment-platform/18-identity-bucketing.md.
|
|
501
514
|
*/
|
|
502
515
|
anonId?: string;
|
|
516
|
+
/**
|
|
517
|
+
* Public client key. When provided, the i18n loader tag can revalidate
|
|
518
|
+
* strings at runtime (`data-key`). Optional — the bootstrap tag NEVER carries
|
|
519
|
+
* a key, and SSR first paint works keyless via `data-strings`.
|
|
520
|
+
*/
|
|
521
|
+
clientKey?: string;
|
|
522
|
+
/** CDN base for the tag `src`s. Defaults to https://cdn.shipeasy.ai. */
|
|
523
|
+
baseUrl?: string;
|
|
524
|
+
}
|
|
525
|
+
interface ScriptTagSpec {
|
|
526
|
+
src: string;
|
|
527
|
+
/** Attribute name → value. An empty-string value renders as a bare boolean attribute (e.g. `data-se-bootstrap`). */
|
|
528
|
+
attrs: Record<string, string>;
|
|
529
|
+
}
|
|
530
|
+
interface BootstrapData {
|
|
531
|
+
/** se-bootstrap.js tag — always present. */
|
|
532
|
+
bootstrap: ScriptTagSpec;
|
|
533
|
+
/** i18n loader tag — null when there are no SSR strings and no client key. */
|
|
534
|
+
i18nLoader: ScriptTagSpec | null;
|
|
503
535
|
}
|
|
504
536
|
/**
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
* server-side). The browser supplies its own client key via
|
|
509
|
-
* `shipeasy({ clientKey })` from @shipeasy/sdk/client, which also injects the
|
|
510
|
-
* runtime i18n loader. This script emits:
|
|
511
|
-
* - window.__se_devtools_config (when devtoolsAdminUrl is set)
|
|
512
|
-
* - window.__SE_BOOTSTRAP (flags + configs + experiments + i18n DATA + i18nProfile, NO key)
|
|
513
|
-
* - window.i18n shim from SSR strings (prevents hydration mismatches / FOUC)
|
|
514
|
-
* - devtools overlay loader when ?se / ?se_devtools is present
|
|
515
|
-
*
|
|
516
|
-
* Framework-agnostic: set innerHTML on a <script> element, nothing else required.
|
|
537
|
+
* Build the structured `<script>` tag specs for the SSR bootstrap. Render
|
|
538
|
+
* these as REAL `<script>` elements (React: scripts set via innerHTML do not
|
|
539
|
+
* execute). No SDK key is ever embedded in the bootstrap tag.
|
|
517
540
|
*/
|
|
518
|
-
declare function
|
|
541
|
+
declare function getBootstrapData(bootstrap: BootstrapPayload | null, i18nData: I18nForRequest | null, opts: BootstrapEmitOptions): BootstrapData;
|
|
542
|
+
/**
|
|
543
|
+
* The bootstrap (and optional i18n loader) tags as an HTML string, for non-React
|
|
544
|
+
* SSR (Express, raw templates) where the markup is emitted directly into the
|
|
545
|
+
* served HTML and executes normally. React callers should use
|
|
546
|
+
* {@link getBootstrapData} and render real `<script>` elements instead.
|
|
547
|
+
*/
|
|
548
|
+
declare function getBootstrapTags(bootstrap: BootstrapPayload | null, i18nData: I18nForRequest | null, opts: BootstrapEmitOptions): string;
|
|
519
549
|
declare const flags: {
|
|
520
|
-
configure(opts:
|
|
550
|
+
configure(opts: EngineOptions): void;
|
|
521
551
|
/**
|
|
522
552
|
* Long-running server: starts the background poll. Call once at app boot.
|
|
523
553
|
* Throws if the initial fetch fails (caller decides whether to crash or degrade).
|
|
@@ -540,7 +570,7 @@ declare const flags: {
|
|
|
540
570
|
ks(name: string, switchKey?: string): boolean;
|
|
541
571
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
542
572
|
/** Emit an exposure for an enrolled experiment at the decision point. See
|
|
543
|
-
* {@link
|
|
573
|
+
* {@link Engine.logExposure}. No-op before configure(). */
|
|
544
574
|
logExposure(user: string | User, name: string): void;
|
|
545
575
|
/**
|
|
546
576
|
* Evaluate all flags / configs / experiments for a user against the locally
|
|
@@ -549,6 +579,65 @@ declare const flags: {
|
|
|
549
579
|
*/
|
|
550
580
|
evaluate(user: User, rawUrl?: string): BootstrapPayload;
|
|
551
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
|
+
/**
|
|
596
|
+
* Configure the SDK once at app boot, then evaluate per user with
|
|
597
|
+
* `new Client(user)`. Builds the process-wide {@link Engine} (polling + blob
|
|
598
|
+
* cache + HTTP) and registers the `attributes` transform. The first call wins;
|
|
599
|
+
* later calls return the existing engine (mirrors {@link configureShipeasyServer}).
|
|
600
|
+
*
|
|
601
|
+
* ```ts
|
|
602
|
+
* import { configure, Client } from "@shipeasy/sdk/server";
|
|
603
|
+
*
|
|
604
|
+
* configure({
|
|
605
|
+
* apiKey: process.env.SHIPEASY_SERVER_KEY!,
|
|
606
|
+
* attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan, country: u.geo.country }),
|
|
607
|
+
* });
|
|
608
|
+
*
|
|
609
|
+
* const flags = new Client(currentUser);
|
|
610
|
+
* if (flags.getFlag("new_checkout")) { ... }
|
|
611
|
+
* ```
|
|
612
|
+
*/
|
|
613
|
+
declare function configure<U = unknown>(opts: ConfigureOptions<U>): Engine;
|
|
614
|
+
/** Test seam: reset the registered attribute transform. */
|
|
615
|
+
declare function _resetConfigureForTests(): void;
|
|
616
|
+
/**
|
|
617
|
+
* A user-bound evaluation handle. Construct one per user/request — it's cheap
|
|
618
|
+
* (it delegates to the {@link Engine} built by {@link configure}); it does NOT
|
|
619
|
+
* open its own connection or poll. The configured `attributes` transform runs
|
|
620
|
+
* once here, so every getFlag/getConfig/getExperiment reads the same bag.
|
|
621
|
+
*
|
|
622
|
+
* ```ts
|
|
623
|
+
* const flags = new Client(req.user);
|
|
624
|
+
* flags.getFlag("new_checkout"); // no user arg — bound at construction
|
|
625
|
+
* flags.getExperiment("price_test", { price: 9 });
|
|
626
|
+
* ```
|
|
627
|
+
*/
|
|
628
|
+
declare class Client<U = unknown> {
|
|
629
|
+
private readonly engine;
|
|
630
|
+
/** The resolved attribute bag this handle evaluates against. */
|
|
631
|
+
readonly attributes: User;
|
|
632
|
+
constructor(user: U);
|
|
633
|
+
getFlag(name: string, defaultValue?: boolean): boolean;
|
|
634
|
+
getFlagDetail(name: string): FlagDetail;
|
|
635
|
+
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
636
|
+
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
637
|
+
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
638
|
+
/** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
|
|
639
|
+
getKillswitch(name: string, switchKey?: string): boolean;
|
|
640
|
+
}
|
|
552
641
|
interface SeeApi {
|
|
553
642
|
/**
|
|
554
643
|
* Report a handled problem and its product consequence:
|
|
@@ -600,4 +689,4 @@ interface SeeApi {
|
|
|
600
689
|
*/
|
|
601
690
|
declare const see: SeeApi;
|
|
602
691
|
|
|
603
|
-
export { ANON_ID_COOKIE, type
|
|
692
|
+
export { ANON_ID_COOKIE, type AttributesFn, type BootstrapData, type BootstrapEmitOptions, type BootstrapPayload, Client, type ConfigureOptions, 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, configure, configureShipeasyServer, createInMemoryStickyStore, fetchLabelsForSSR, flags, getBootstrapData, getBootstrapTags, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
|
package/dist/server/index.js
CHANGED
|
@@ -31,15 +31,19 @@ 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
|
+
configure: () => configure,
|
|
38
41
|
configureShipeasyServer: () => configureShipeasyServer,
|
|
39
42
|
createInMemoryStickyStore: () => createInMemoryStickyStore,
|
|
40
43
|
fetchLabelsForSSR: () => fetchLabelsForSSR,
|
|
41
44
|
flags: () => flags,
|
|
42
|
-
|
|
45
|
+
getBootstrapData: () => getBootstrapData,
|
|
46
|
+
getBootstrapTags: () => getBootstrapTags,
|
|
43
47
|
getShipeasyServerClient: () => getShipeasyServerClient,
|
|
44
48
|
i18n: () => i18n,
|
|
45
49
|
isExpected: () => isExpected,
|
|
@@ -542,7 +546,7 @@ function parseOverrides(rawUrl) {
|
|
|
542
546
|
}
|
|
543
547
|
return { gates, configs, experiments };
|
|
544
548
|
}
|
|
545
|
-
var
|
|
549
|
+
var Engine = class _Engine {
|
|
546
550
|
apiKey;
|
|
547
551
|
baseUrl;
|
|
548
552
|
env;
|
|
@@ -557,7 +561,7 @@ var FlagsClient = class _FlagsClient {
|
|
|
557
561
|
pollInterval = 30;
|
|
558
562
|
timer = null;
|
|
559
563
|
initialized = false;
|
|
560
|
-
// Test mode: built by `
|
|
564
|
+
// Test mode: built by `Engine.forTesting()`. When set, init()/initOnce()
|
|
561
565
|
// never fetch, track() is a no-op, and telemetry is off — the client is a
|
|
562
566
|
// fully self-contained, network-free seam for unit tests.
|
|
563
567
|
testMode;
|
|
@@ -598,13 +602,13 @@ var FlagsClient = class _FlagsClient {
|
|
|
598
602
|
* with overrideFlag/overrideConfig/overrideExperiment. No SDK key required.
|
|
599
603
|
*
|
|
600
604
|
* ```ts
|
|
601
|
-
* const client =
|
|
605
|
+
* const client = Engine.forTesting();
|
|
602
606
|
* client.overrideFlag("new_checkout", true);
|
|
603
607
|
* client.getFlag("new_checkout", { user_id: "u1" }); // true
|
|
604
608
|
* ```
|
|
605
609
|
*/
|
|
606
610
|
static forTesting(opts) {
|
|
607
|
-
return new
|
|
611
|
+
return new _Engine({ apiKey: "", ...opts, testMode: true });
|
|
608
612
|
}
|
|
609
613
|
/**
|
|
610
614
|
* Build a fully OFFLINE client from a pre-captured snapshot — no network ever.
|
|
@@ -617,7 +621,7 @@ var FlagsClient = class _FlagsClient {
|
|
|
617
621
|
* `{ flags: <GET /sdk/flags body>, experiments: <GET /sdk/experiments body> }`.
|
|
618
622
|
*/
|
|
619
623
|
static fromSnapshot(snapshot) {
|
|
620
|
-
const client = new
|
|
624
|
+
const client = new _Engine({ apiKey: "", testMode: true });
|
|
621
625
|
client.flagsBlob = snapshot.flags;
|
|
622
626
|
client.expsBlob = snapshot.experiments;
|
|
623
627
|
client.initialized = true;
|
|
@@ -627,13 +631,13 @@ var FlagsClient = class _FlagsClient {
|
|
|
627
631
|
* Build a fully OFFLINE client from a snapshot JSON file on disk (Node only —
|
|
628
632
|
* not available in the browser entrypoint). The file must contain
|
|
629
633
|
* `{ "flags": <GET /sdk/flags body>, "experiments": <GET /sdk/experiments body> }`.
|
|
630
|
-
* See {@link
|
|
634
|
+
* See {@link Engine.fromSnapshot}.
|
|
631
635
|
*/
|
|
632
636
|
static fromFile(path) {
|
|
633
637
|
const fs = require("fs");
|
|
634
638
|
const raw = fs.readFileSync(path, "utf8");
|
|
635
639
|
const snapshot = JSON.parse(raw);
|
|
636
|
-
return
|
|
640
|
+
return _Engine.fromSnapshot(snapshot);
|
|
637
641
|
}
|
|
638
642
|
async init() {
|
|
639
643
|
if (this.testMode) {
|
|
@@ -1093,7 +1097,7 @@ async function fetchLabelsForSSR(opts) {
|
|
|
1093
1097
|
var _server = null;
|
|
1094
1098
|
function configureShipeasyServer(opts) {
|
|
1095
1099
|
if (_server) return _server;
|
|
1096
|
-
_server = new
|
|
1100
|
+
_server = new Engine(opts);
|
|
1097
1101
|
return _server;
|
|
1098
1102
|
}
|
|
1099
1103
|
function getShipeasyServerClient() {
|
|
@@ -1161,46 +1165,62 @@ async function shipeasy(opts) {
|
|
|
1161
1165
|
flags: bootstrap.flags,
|
|
1162
1166
|
configs: bootstrap.configs,
|
|
1163
1167
|
experiments: bootstrap.experiments,
|
|
1164
|
-
|
|
1165
|
-
return
|
|
1166
|
-
editLabels,
|
|
1168
|
+
getBootstrapData(emit) {
|
|
1169
|
+
return getBootstrapData(bootstrap, i18nData, {
|
|
1167
1170
|
i18nProfile: profile,
|
|
1168
|
-
anonId
|
|
1171
|
+
anonId,
|
|
1172
|
+
...emit
|
|
1173
|
+
});
|
|
1174
|
+
},
|
|
1175
|
+
getBootstrapTags(emit) {
|
|
1176
|
+
return getBootstrapTags(bootstrap, i18nData, {
|
|
1177
|
+
i18nProfile: profile,
|
|
1178
|
+
anonId,
|
|
1179
|
+
...emit
|
|
1169
1180
|
});
|
|
1170
1181
|
}
|
|
1171
1182
|
};
|
|
1172
1183
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
const
|
|
1184
|
+
var DEFAULT_CDN = "https://cdn.shipeasy.ai";
|
|
1185
|
+
function getBootstrapData(bootstrap, i18nData, opts) {
|
|
1186
|
+
const base = (opts.baseUrl ?? DEFAULT_CDN).replace(/\/$/, "");
|
|
1176
1187
|
const profile = opts.i18nProfile ?? "en:prod";
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1188
|
+
const attrs = {
|
|
1189
|
+
"data-se-bootstrap": "",
|
|
1190
|
+
"data-flags": JSON.stringify(bootstrap?.flags ?? {}),
|
|
1191
|
+
"data-configs": JSON.stringify(bootstrap?.configs ?? {}),
|
|
1192
|
+
"data-experiments": JSON.stringify(bootstrap?.experiments ?? {}),
|
|
1193
|
+
"data-killswitches": JSON.stringify(bootstrap?.killswitches ?? {}),
|
|
1194
|
+
"data-i18n-profile": profile,
|
|
1195
|
+
"data-api-url": base
|
|
1185
1196
|
};
|
|
1186
|
-
if (
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
if (i18nData?.strings && Object.keys(i18nData.strings).length > 0) {
|
|
1199
|
-
parts.push(
|
|
1200
|
-
`(function(){var d=window.__SE_BOOTSTRAP.i18n;if(!d)return;window.i18n={locale:d.locale,t:function(k,v){var r=d.strings[k];if(!r)return k;return v?r.replace(/\\{\\{(\\w+)\\}\\}/g,function(_,p){return v[p]!==undefined?String(v[p]):'{{'+p+'}}'}):r;},on:function(){return function(){};}};})();`
|
|
1201
|
-
);
|
|
1197
|
+
if (opts.anonId) attrs["data-anon-id"] = opts.anonId;
|
|
1198
|
+
const bootstrapTag = { src: `${base}/sdk/bootstrap.js`, attrs };
|
|
1199
|
+
let i18nLoader = null;
|
|
1200
|
+
const hasStrings = !!(i18nData?.strings && Object.keys(i18nData.strings).length > 0);
|
|
1201
|
+
if (hasStrings || opts.clientKey) {
|
|
1202
|
+
const i18nAttrs = { "data-profile": profile };
|
|
1203
|
+
if (opts.clientKey) i18nAttrs["data-key"] = opts.clientKey;
|
|
1204
|
+
if (hasStrings) {
|
|
1205
|
+
i18nAttrs["data-strings"] = JSON.stringify(i18nData.strings);
|
|
1206
|
+
i18nAttrs["data-locale"] = i18nData.locale;
|
|
1207
|
+
}
|
|
1208
|
+
i18nLoader = { src: `${base}/sdk/i18n/loader.js`, attrs: i18nAttrs };
|
|
1202
1209
|
}
|
|
1203
|
-
return
|
|
1210
|
+
return { bootstrap: bootstrapTag, i18nLoader };
|
|
1211
|
+
}
|
|
1212
|
+
function escapeAttr(v) {
|
|
1213
|
+
return v.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1214
|
+
}
|
|
1215
|
+
function renderScriptTag(spec) {
|
|
1216
|
+
const attrs = Object.entries(spec.attrs).map(([k, v]) => v === "" ? ` ${k}` : ` ${k}="${escapeAttr(v)}"`).join("");
|
|
1217
|
+
return `<script src="${escapeAttr(spec.src)}"${attrs}></script>`;
|
|
1218
|
+
}
|
|
1219
|
+
function getBootstrapTags(bootstrap, i18nData, opts) {
|
|
1220
|
+
const data = getBootstrapData(bootstrap, i18nData, opts);
|
|
1221
|
+
const tags = [data.bootstrap];
|
|
1222
|
+
if (data.i18nLoader) tags.push(data.i18nLoader);
|
|
1223
|
+
return tags.map(renderScriptTag).join("");
|
|
1204
1224
|
}
|
|
1205
1225
|
var flags = {
|
|
1206
1226
|
configure(opts) {
|
|
@@ -1252,7 +1272,7 @@ var flags = {
|
|
|
1252
1272
|
_server?.track(userId, eventName, props);
|
|
1253
1273
|
},
|
|
1254
1274
|
/** Emit an exposure for an enrolled experiment at the decision point. See
|
|
1255
|
-
* {@link
|
|
1275
|
+
* {@link Engine.logExposure}. No-op before configure(). */
|
|
1256
1276
|
logExposure(user, name) {
|
|
1257
1277
|
_server?.logExposure(user, name);
|
|
1258
1278
|
},
|
|
@@ -1270,6 +1290,50 @@ var flags = {
|
|
|
1270
1290
|
};
|
|
1271
1291
|
}
|
|
1272
1292
|
};
|
|
1293
|
+
var _identityAttributes = (user) => user && typeof user === "object" ? user : {};
|
|
1294
|
+
var _attributes = _identityAttributes;
|
|
1295
|
+
function configure(opts) {
|
|
1296
|
+
const { attributes, ...engineOpts } = opts;
|
|
1297
|
+
_attributes = attributes ?? _identityAttributes;
|
|
1298
|
+
const engine = configureShipeasyServer(engineOpts);
|
|
1299
|
+
void engine.initOnce().catch(() => {
|
|
1300
|
+
});
|
|
1301
|
+
return engine;
|
|
1302
|
+
}
|
|
1303
|
+
function _resetConfigureForTests() {
|
|
1304
|
+
_attributes = _identityAttributes;
|
|
1305
|
+
}
|
|
1306
|
+
var Client = class {
|
|
1307
|
+
engine;
|
|
1308
|
+
/** The resolved attribute bag this handle evaluates against. */
|
|
1309
|
+
attributes;
|
|
1310
|
+
constructor(user) {
|
|
1311
|
+
const engine = getShipeasyServerClient();
|
|
1312
|
+
if (!engine) {
|
|
1313
|
+
throw new Error(
|
|
1314
|
+
"[shipeasy] new Client(user) called before configure({ apiKey }). Call configure() once at app boot from @shipeasy/sdk/server."
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
this.engine = engine;
|
|
1318
|
+
this.attributes = _attributes(user);
|
|
1319
|
+
}
|
|
1320
|
+
getFlag(name, defaultValue = false) {
|
|
1321
|
+
return this.engine.getFlag(name, this.attributes, defaultValue);
|
|
1322
|
+
}
|
|
1323
|
+
getFlagDetail(name) {
|
|
1324
|
+
return this.engine.getFlagDetail(name, this.attributes);
|
|
1325
|
+
}
|
|
1326
|
+
getConfig(name, decodeOrOpts) {
|
|
1327
|
+
return this.engine.getConfig(name, decodeOrOpts);
|
|
1328
|
+
}
|
|
1329
|
+
getExperiment(name, defaultParams, decode) {
|
|
1330
|
+
return this.engine.getExperiment(name, this.attributes, defaultParams, decode);
|
|
1331
|
+
}
|
|
1332
|
+
/** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
|
|
1333
|
+
getKillswitch(name, switchKey) {
|
|
1334
|
+
return this.engine.getKillswitch(name, switchKey);
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1273
1337
|
function dispatchSee(problem, consequence, extras, kind) {
|
|
1274
1338
|
if (!_server) {
|
|
1275
1339
|
console.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
|
|
@@ -1287,15 +1351,19 @@ var see = Object.assign(
|
|
|
1287
1351
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1288
1352
|
0 && (module.exports = {
|
|
1289
1353
|
ANON_ID_COOKIE,
|
|
1354
|
+
Client,
|
|
1355
|
+
Engine,
|
|
1290
1356
|
FLAG_REASONS,
|
|
1291
|
-
FlagsClient,
|
|
1292
1357
|
_murmur3ForTests,
|
|
1358
|
+
_resetConfigureForTests,
|
|
1293
1359
|
_resetShipeasyServerForTests,
|
|
1360
|
+
configure,
|
|
1294
1361
|
configureShipeasyServer,
|
|
1295
1362
|
createInMemoryStickyStore,
|
|
1296
1363
|
fetchLabelsForSSR,
|
|
1297
1364
|
flags,
|
|
1298
|
-
|
|
1365
|
+
getBootstrapData,
|
|
1366
|
+
getBootstrapTags,
|
|
1299
1367
|
getShipeasyServerClient,
|
|
1300
1368
|
i18n,
|
|
1301
1369
|
isExpected,
|