@shipeasy/sdk 2.0.1 → 2.0.3
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/dist/client/index.d.mts +20 -4
- package/dist/client/index.d.ts +20 -4
- package/dist/client/index.js +26 -4
- package/dist/client/index.mjs +26 -4
- package/dist/server/index.d.mts +22 -1
- package/dist/server/index.d.ts +22 -1
- package/dist/server/index.js +85 -0
- package/dist/server/index.mjs +85 -0
- package/package.json +1 -1
package/dist/client/index.d.mts
CHANGED
|
@@ -118,6 +118,15 @@ declare function getShipeasyClient(): FlagsClientBrowser | null;
|
|
|
118
118
|
* Not part of the documented surface; production code should never call this.
|
|
119
119
|
*/
|
|
120
120
|
declare function _resetShipeasyForTests(): void;
|
|
121
|
+
interface BootstrapPayload {
|
|
122
|
+
flags: Record<string, boolean>;
|
|
123
|
+
configs: Record<string, unknown>;
|
|
124
|
+
experiments: Record<string, {
|
|
125
|
+
inExperiment: boolean;
|
|
126
|
+
group: string;
|
|
127
|
+
params: Record<string, unknown>;
|
|
128
|
+
}>;
|
|
129
|
+
}
|
|
121
130
|
/**
|
|
122
131
|
* Universal flags facade. Methods return safe defaults when the singleton
|
|
123
132
|
* hasn't been configured yet (false / undefined / `notIn` experiment), so
|
|
@@ -127,8 +136,11 @@ declare const flags: {
|
|
|
127
136
|
configure(opts: FlagsClientBrowserOptions): void;
|
|
128
137
|
identify(user: User): Promise<void>;
|
|
129
138
|
/**
|
|
130
|
-
* Read a feature gate.
|
|
131
|
-
*
|
|
139
|
+
* Read a feature gate.
|
|
140
|
+
* Priority: bootstrap → CDN/URL-override (post-mount) → false.
|
|
141
|
+
* Bootstrap is safe before mount because the server rendered with the same values.
|
|
142
|
+
* Everything else gates on _mountedAndReady to prevent hydration mismatches on
|
|
143
|
+
* force-static pages where SSR has no flag data.
|
|
132
144
|
*/
|
|
133
145
|
get(name: string): boolean;
|
|
134
146
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
@@ -138,7 +150,11 @@ declare const flags: {
|
|
|
138
150
|
/**
|
|
139
151
|
* Called by FlagsBoundary after React hydration to unlock flag reads.
|
|
140
152
|
* Dispatches se:override:change so subscribers (FlagsBoundary) re-render
|
|
141
|
-
* once with real values — URL overrides and
|
|
153
|
+
* once with real values — URL overrides and CDN-loaded flags.
|
|
154
|
+
*
|
|
155
|
+
* Always dispatches even if already mounted: in React hydration-recovery
|
|
156
|
+
* renders the latch is already true so the early-return guard would swallow
|
|
157
|
+
* the event, leaving the re-mounted subtree stuck with stale (empty) values.
|
|
142
158
|
*/
|
|
143
159
|
notifyMounted(): void;
|
|
144
160
|
/** Subscribe for change notifications (identify/override). Used by framework adapters. */
|
|
@@ -190,4 +206,4 @@ declare const i18n: {
|
|
|
190
206
|
onUpdate(cb: () => void): () => void;
|
|
191
207
|
};
|
|
192
208
|
|
|
193
|
-
export { type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type ShipeasySdkBridge, type User, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, version };
|
|
209
|
+
export { type BootstrapPayload, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type ShipeasySdkBridge, type User, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, version };
|
package/dist/client/index.d.ts
CHANGED
|
@@ -118,6 +118,15 @@ declare function getShipeasyClient(): FlagsClientBrowser | null;
|
|
|
118
118
|
* Not part of the documented surface; production code should never call this.
|
|
119
119
|
*/
|
|
120
120
|
declare function _resetShipeasyForTests(): void;
|
|
121
|
+
interface BootstrapPayload {
|
|
122
|
+
flags: Record<string, boolean>;
|
|
123
|
+
configs: Record<string, unknown>;
|
|
124
|
+
experiments: Record<string, {
|
|
125
|
+
inExperiment: boolean;
|
|
126
|
+
group: string;
|
|
127
|
+
params: Record<string, unknown>;
|
|
128
|
+
}>;
|
|
129
|
+
}
|
|
121
130
|
/**
|
|
122
131
|
* Universal flags facade. Methods return safe defaults when the singleton
|
|
123
132
|
* hasn't been configured yet (false / undefined / `notIn` experiment), so
|
|
@@ -127,8 +136,11 @@ declare const flags: {
|
|
|
127
136
|
configure(opts: FlagsClientBrowserOptions): void;
|
|
128
137
|
identify(user: User): Promise<void>;
|
|
129
138
|
/**
|
|
130
|
-
* Read a feature gate.
|
|
131
|
-
*
|
|
139
|
+
* Read a feature gate.
|
|
140
|
+
* Priority: bootstrap → CDN/URL-override (post-mount) → false.
|
|
141
|
+
* Bootstrap is safe before mount because the server rendered with the same values.
|
|
142
|
+
* Everything else gates on _mountedAndReady to prevent hydration mismatches on
|
|
143
|
+
* force-static pages where SSR has no flag data.
|
|
132
144
|
*/
|
|
133
145
|
get(name: string): boolean;
|
|
134
146
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
@@ -138,7 +150,11 @@ declare const flags: {
|
|
|
138
150
|
/**
|
|
139
151
|
* Called by FlagsBoundary after React hydration to unlock flag reads.
|
|
140
152
|
* Dispatches se:override:change so subscribers (FlagsBoundary) re-render
|
|
141
|
-
* once with real values — URL overrides and
|
|
153
|
+
* once with real values — URL overrides and CDN-loaded flags.
|
|
154
|
+
*
|
|
155
|
+
* Always dispatches even if already mounted: in React hydration-recovery
|
|
156
|
+
* renders the latch is already true so the early-return guard would swallow
|
|
157
|
+
* the event, leaving the re-mounted subtree stuck with stale (empty) values.
|
|
142
158
|
*/
|
|
143
159
|
notifyMounted(): void;
|
|
144
160
|
/** Subscribe for change notifications (identify/override). Used by framework adapters. */
|
|
@@ -190,4 +206,4 @@ declare const i18n: {
|
|
|
190
206
|
onUpdate(cb: () => void): () => void;
|
|
191
207
|
};
|
|
192
208
|
|
|
193
|
-
export { type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type ShipeasySdkBridge, type User, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, version };
|
|
209
|
+
export { type BootstrapPayload, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type ShipeasySdkBridge, type User, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, version };
|
package/dist/client/index.js
CHANGED
|
@@ -659,6 +659,10 @@ function _resetShipeasyForTests() {
|
|
|
659
659
|
_client?.destroy();
|
|
660
660
|
_client = null;
|
|
661
661
|
}
|
|
662
|
+
function getBootstrap() {
|
|
663
|
+
if (typeof window === "undefined") return null;
|
|
664
|
+
return window.__SE_BOOTSTRAP ?? null;
|
|
665
|
+
}
|
|
662
666
|
var _mountedAndReady = false;
|
|
663
667
|
var _standaloneListeners = /* @__PURE__ */ new Set();
|
|
664
668
|
var _standaloneOverrideWired = false;
|
|
@@ -681,15 +685,30 @@ var flags = {
|
|
|
681
685
|
return _client.identify(user);
|
|
682
686
|
},
|
|
683
687
|
/**
|
|
684
|
-
* Read a feature gate.
|
|
685
|
-
*
|
|
688
|
+
* Read a feature gate.
|
|
689
|
+
* Priority: bootstrap → CDN/URL-override (post-mount) → false.
|
|
690
|
+
* Bootstrap is safe before mount because the server rendered with the same values.
|
|
691
|
+
* Everything else gates on _mountedAndReady to prevent hydration mismatches on
|
|
692
|
+
* force-static pages where SSR has no flag data.
|
|
686
693
|
*/
|
|
687
694
|
get(name) {
|
|
695
|
+
const bs = getBootstrap();
|
|
696
|
+
if (bs !== null && name in bs.flags) return bs.flags[name];
|
|
688
697
|
if (!_mountedAndReady) return false;
|
|
689
698
|
if (_client) return _client.getFlag(name);
|
|
690
699
|
return readGateOverride(name) ?? false;
|
|
691
700
|
},
|
|
692
701
|
getConfig(name, decode) {
|
|
702
|
+
const bs = getBootstrap();
|
|
703
|
+
if (bs !== null && name in bs.configs) {
|
|
704
|
+
const raw = bs.configs[name];
|
|
705
|
+
if (!decode) return raw;
|
|
706
|
+
try {
|
|
707
|
+
return decode(raw);
|
|
708
|
+
} catch {
|
|
709
|
+
return void 0;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
693
712
|
if (!_mountedAndReady) return void 0;
|
|
694
713
|
if (_client) return _client.getConfig(name, decode);
|
|
695
714
|
const ov = readConfigOverride(name);
|
|
@@ -717,10 +736,13 @@ var flags = {
|
|
|
717
736
|
/**
|
|
718
737
|
* Called by FlagsBoundary after React hydration to unlock flag reads.
|
|
719
738
|
* Dispatches se:override:change so subscribers (FlagsBoundary) re-render
|
|
720
|
-
* once with real values — URL overrides and
|
|
739
|
+
* once with real values — URL overrides and CDN-loaded flags.
|
|
740
|
+
*
|
|
741
|
+
* Always dispatches even if already mounted: in React hydration-recovery
|
|
742
|
+
* renders the latch is already true so the early-return guard would swallow
|
|
743
|
+
* the event, leaving the re-mounted subtree stuck with stale (empty) values.
|
|
721
744
|
*/
|
|
722
745
|
notifyMounted() {
|
|
723
|
-
if (_mountedAndReady) return;
|
|
724
746
|
_mountedAndReady = true;
|
|
725
747
|
if (typeof window !== "undefined") {
|
|
726
748
|
window.dispatchEvent(new CustomEvent("se:override:change"));
|
package/dist/client/index.mjs
CHANGED
|
@@ -617,6 +617,10 @@ function _resetShipeasyForTests() {
|
|
|
617
617
|
_client?.destroy();
|
|
618
618
|
_client = null;
|
|
619
619
|
}
|
|
620
|
+
function getBootstrap() {
|
|
621
|
+
if (typeof window === "undefined") return null;
|
|
622
|
+
return window.__SE_BOOTSTRAP ?? null;
|
|
623
|
+
}
|
|
620
624
|
var _mountedAndReady = false;
|
|
621
625
|
var _standaloneListeners = /* @__PURE__ */ new Set();
|
|
622
626
|
var _standaloneOverrideWired = false;
|
|
@@ -639,15 +643,30 @@ var flags = {
|
|
|
639
643
|
return _client.identify(user);
|
|
640
644
|
},
|
|
641
645
|
/**
|
|
642
|
-
* Read a feature gate.
|
|
643
|
-
*
|
|
646
|
+
* Read a feature gate.
|
|
647
|
+
* Priority: bootstrap → CDN/URL-override (post-mount) → false.
|
|
648
|
+
* Bootstrap is safe before mount because the server rendered with the same values.
|
|
649
|
+
* Everything else gates on _mountedAndReady to prevent hydration mismatches on
|
|
650
|
+
* force-static pages where SSR has no flag data.
|
|
644
651
|
*/
|
|
645
652
|
get(name) {
|
|
653
|
+
const bs = getBootstrap();
|
|
654
|
+
if (bs !== null && name in bs.flags) return bs.flags[name];
|
|
646
655
|
if (!_mountedAndReady) return false;
|
|
647
656
|
if (_client) return _client.getFlag(name);
|
|
648
657
|
return readGateOverride(name) ?? false;
|
|
649
658
|
},
|
|
650
659
|
getConfig(name, decode) {
|
|
660
|
+
const bs = getBootstrap();
|
|
661
|
+
if (bs !== null && name in bs.configs) {
|
|
662
|
+
const raw = bs.configs[name];
|
|
663
|
+
if (!decode) return raw;
|
|
664
|
+
try {
|
|
665
|
+
return decode(raw);
|
|
666
|
+
} catch {
|
|
667
|
+
return void 0;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
651
670
|
if (!_mountedAndReady) return void 0;
|
|
652
671
|
if (_client) return _client.getConfig(name, decode);
|
|
653
672
|
const ov = readConfigOverride(name);
|
|
@@ -675,10 +694,13 @@ var flags = {
|
|
|
675
694
|
/**
|
|
676
695
|
* Called by FlagsBoundary after React hydration to unlock flag reads.
|
|
677
696
|
* Dispatches se:override:change so subscribers (FlagsBoundary) re-render
|
|
678
|
-
* once with real values — URL overrides and
|
|
697
|
+
* once with real values — URL overrides and CDN-loaded flags.
|
|
698
|
+
*
|
|
699
|
+
* Always dispatches even if already mounted: in React hydration-recovery
|
|
700
|
+
* renders the latch is already true so the early-return guard would swallow
|
|
701
|
+
* the event, leaving the re-mounted subtree stuck with stale (empty) values.
|
|
679
702
|
*/
|
|
680
703
|
notifyMounted() {
|
|
681
|
-
if (_mountedAndReady) return;
|
|
682
704
|
_mountedAndReady = true;
|
|
683
705
|
if (typeof window !== "undefined") {
|
|
684
706
|
window.dispatchEvent(new CustomEvent("se:override:change"));
|
package/dist/server/index.d.mts
CHANGED
|
@@ -9,6 +9,11 @@ interface ExperimentResult<P> {
|
|
|
9
9
|
group: string;
|
|
10
10
|
params: P;
|
|
11
11
|
}
|
|
12
|
+
interface BootstrapPayload {
|
|
13
|
+
flags: Record<string, boolean>;
|
|
14
|
+
configs: Record<string, unknown>;
|
|
15
|
+
experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
|
|
16
|
+
}
|
|
12
17
|
type FlagsClientEnv = "dev" | "staging" | "prod";
|
|
13
18
|
interface FlagsClientOptions {
|
|
14
19
|
apiKey: string;
|
|
@@ -39,6 +44,16 @@ declare class FlagsClient {
|
|
|
39
44
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
40
45
|
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
41
46
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
47
|
+
/**
|
|
48
|
+
* Evaluate all flags, configs, and experiments for a user against the locally
|
|
49
|
+
* cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
|
|
50
|
+
* overrides from the request URL when provided.
|
|
51
|
+
*
|
|
52
|
+
* Intended for SSR: call on the server, inject the result as
|
|
53
|
+
* `window.__SE_BOOTSTRAP` in the HTML, and the client SDK will read it
|
|
54
|
+
* synchronously without waiting for identify() to resolve.
|
|
55
|
+
*/
|
|
56
|
+
evaluate(user: User, rawUrl?: string): BootstrapPayload;
|
|
42
57
|
}
|
|
43
58
|
interface LabelFile {
|
|
44
59
|
v: number;
|
|
@@ -72,6 +87,12 @@ declare const flags: {
|
|
|
72
87
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
73
88
|
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
74
89
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
90
|
+
/**
|
|
91
|
+
* Evaluate all flags / configs / experiments for a user against the locally
|
|
92
|
+
* cached blob. Pass the request URL to apply ?se_ks_* / ?se_cf_* / ?se_exp_*
|
|
93
|
+
* overrides. Returns an empty payload when the blob hasn't been fetched yet.
|
|
94
|
+
*/
|
|
95
|
+
evaluate(user: User, rawUrl?: string): BootstrapPayload;
|
|
75
96
|
};
|
|
76
97
|
|
|
77
|
-
export { type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type LabelFile, type User, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getShipeasyServerClient, version };
|
|
98
|
+
export { type BootstrapPayload, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type LabelFile, type User, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getShipeasyServerClient, version };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -9,6 +9,11 @@ interface ExperimentResult<P> {
|
|
|
9
9
|
group: string;
|
|
10
10
|
params: P;
|
|
11
11
|
}
|
|
12
|
+
interface BootstrapPayload {
|
|
13
|
+
flags: Record<string, boolean>;
|
|
14
|
+
configs: Record<string, unknown>;
|
|
15
|
+
experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
|
|
16
|
+
}
|
|
12
17
|
type FlagsClientEnv = "dev" | "staging" | "prod";
|
|
13
18
|
interface FlagsClientOptions {
|
|
14
19
|
apiKey: string;
|
|
@@ -39,6 +44,16 @@ declare class FlagsClient {
|
|
|
39
44
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
40
45
|
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
41
46
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
47
|
+
/**
|
|
48
|
+
* Evaluate all flags, configs, and experiments for a user against the locally
|
|
49
|
+
* cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
|
|
50
|
+
* overrides from the request URL when provided.
|
|
51
|
+
*
|
|
52
|
+
* Intended for SSR: call on the server, inject the result as
|
|
53
|
+
* `window.__SE_BOOTSTRAP` in the HTML, and the client SDK will read it
|
|
54
|
+
* synchronously without waiting for identify() to resolve.
|
|
55
|
+
*/
|
|
56
|
+
evaluate(user: User, rawUrl?: string): BootstrapPayload;
|
|
42
57
|
}
|
|
43
58
|
interface LabelFile {
|
|
44
59
|
v: number;
|
|
@@ -72,6 +87,12 @@ declare const flags: {
|
|
|
72
87
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
73
88
|
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
74
89
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
90
|
+
/**
|
|
91
|
+
* Evaluate all flags / configs / experiments for a user against the locally
|
|
92
|
+
* cached blob. Pass the request URL to apply ?se_ks_* / ?se_cf_* / ?se_exp_*
|
|
93
|
+
* overrides. Returns an empty payload when the blob hasn't been fetched yet.
|
|
94
|
+
*/
|
|
95
|
+
evaluate(user: User, rawUrl?: string): BootstrapPayload;
|
|
75
96
|
};
|
|
76
97
|
|
|
77
|
-
export { type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type LabelFile, type User, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getShipeasyServerClient, version };
|
|
98
|
+
export { type BootstrapPayload, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type LabelFile, type User, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getShipeasyServerClient, version };
|
package/dist/server/index.js
CHANGED
|
@@ -129,6 +129,51 @@ function evalGateInternal(gate, user) {
|
|
|
129
129
|
if (!uid) return false;
|
|
130
130
|
return murmur3(`${gate.salt}:${uid}`) % 1e4 < gate.rolloutPct;
|
|
131
131
|
}
|
|
132
|
+
var TRUE_RX = /^(true|on|1|yes)$/i;
|
|
133
|
+
var FALSE_RX = /^(false|off|0|no)$/i;
|
|
134
|
+
function parseOverrideBool(raw) {
|
|
135
|
+
if (TRUE_RX.test(raw)) return true;
|
|
136
|
+
if (FALSE_RX.test(raw)) return false;
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
function decodeOverrideConfigValue(raw) {
|
|
140
|
+
if (raw.startsWith("b64:")) {
|
|
141
|
+
try {
|
|
142
|
+
const json = atob(raw.slice(4).replace(/-/g, "+").replace(/_/g, "/"));
|
|
143
|
+
return JSON.parse(json);
|
|
144
|
+
} catch {
|
|
145
|
+
return raw;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
return JSON.parse(raw);
|
|
150
|
+
} catch {
|
|
151
|
+
return raw;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function parseOverrides(rawUrl) {
|
|
155
|
+
const gates = {};
|
|
156
|
+
const configs = {};
|
|
157
|
+
const experiments = {};
|
|
158
|
+
try {
|
|
159
|
+
const url = new URL(rawUrl, "http://localhost");
|
|
160
|
+
for (const [k, v] of url.searchParams) {
|
|
161
|
+
if (k.startsWith("se_ks_")) {
|
|
162
|
+
const b = parseOverrideBool(v);
|
|
163
|
+
if (b !== null) gates[k.slice(6)] = b;
|
|
164
|
+
} else if (k.startsWith("se_cf_")) {
|
|
165
|
+
configs[k.slice(6)] = decodeOverrideConfigValue(v);
|
|
166
|
+
} else if (k.startsWith("se_config_")) {
|
|
167
|
+
configs[k.slice(10)] = decodeOverrideConfigValue(v);
|
|
168
|
+
} else if (k.startsWith("se_exp_")) {
|
|
169
|
+
const name = k.slice(7);
|
|
170
|
+
if (v && v !== "default" && v !== "none") experiments[name] = v;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
175
|
+
return { gates, configs, experiments };
|
|
176
|
+
}
|
|
132
177
|
var FlagsClient = class {
|
|
133
178
|
apiKey;
|
|
134
179
|
baseUrl;
|
|
@@ -271,6 +316,38 @@ var FlagsClient = class {
|
|
|
271
316
|
body
|
|
272
317
|
}).catch((err) => console.warn("[shipeasy] track failed:", String(err)));
|
|
273
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* Evaluate all flags, configs, and experiments for a user against the locally
|
|
321
|
+
* cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
|
|
322
|
+
* overrides from the request URL when provided.
|
|
323
|
+
*
|
|
324
|
+
* Intended for SSR: call on the server, inject the result as
|
|
325
|
+
* `window.__SE_BOOTSTRAP` in the HTML, and the client SDK will read it
|
|
326
|
+
* synchronously without waiting for identify() to resolve.
|
|
327
|
+
*/
|
|
328
|
+
evaluate(user, rawUrl) {
|
|
329
|
+
const flags2 = {};
|
|
330
|
+
const configs = {};
|
|
331
|
+
const experiments = {};
|
|
332
|
+
for (const [name, gate] of Object.entries(this.flagsBlob?.gates ?? {})) {
|
|
333
|
+
flags2[name] = evalGateInternal(gate, user);
|
|
334
|
+
}
|
|
335
|
+
for (const [name, entry] of Object.entries(this.flagsBlob?.configs ?? {})) {
|
|
336
|
+
configs[name] = entry.value;
|
|
337
|
+
}
|
|
338
|
+
for (const [name] of Object.entries(this.expsBlob?.experiments ?? {})) {
|
|
339
|
+
experiments[name] = this.getExperiment(name, user, {});
|
|
340
|
+
}
|
|
341
|
+
if (rawUrl) {
|
|
342
|
+
const ov = parseOverrides(rawUrl);
|
|
343
|
+
Object.assign(flags2, ov.gates);
|
|
344
|
+
Object.assign(configs, ov.configs);
|
|
345
|
+
for (const [name, group] of Object.entries(ov.experiments)) {
|
|
346
|
+
experiments[name] = { inExperiment: true, group, params: {} };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return { flags: flags2, configs, experiments };
|
|
350
|
+
}
|
|
274
351
|
};
|
|
275
352
|
var DEFAULT_I18N_CDN = "https://cdn.i18n.shipeasy.ai";
|
|
276
353
|
async function fetchJson(url, timeoutMs = 2e3) {
|
|
@@ -351,6 +428,14 @@ var flags = {
|
|
|
351
428
|
},
|
|
352
429
|
track(userId, eventName, props) {
|
|
353
430
|
_server?.track(userId, eventName, props);
|
|
431
|
+
},
|
|
432
|
+
/**
|
|
433
|
+
* Evaluate all flags / configs / experiments for a user against the locally
|
|
434
|
+
* cached blob. Pass the request URL to apply ?se_ks_* / ?se_cf_* / ?se_exp_*
|
|
435
|
+
* overrides. Returns an empty payload when the blob hasn't been fetched yet.
|
|
436
|
+
*/
|
|
437
|
+
evaluate(user, rawUrl) {
|
|
438
|
+
return _server?.evaluate(user, rawUrl) ?? { flags: {}, configs: {}, experiments: {} };
|
|
354
439
|
}
|
|
355
440
|
};
|
|
356
441
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/server/index.mjs
CHANGED
|
@@ -99,6 +99,51 @@ function evalGateInternal(gate, user) {
|
|
|
99
99
|
if (!uid) return false;
|
|
100
100
|
return murmur3(`${gate.salt}:${uid}`) % 1e4 < gate.rolloutPct;
|
|
101
101
|
}
|
|
102
|
+
var TRUE_RX = /^(true|on|1|yes)$/i;
|
|
103
|
+
var FALSE_RX = /^(false|off|0|no)$/i;
|
|
104
|
+
function parseOverrideBool(raw) {
|
|
105
|
+
if (TRUE_RX.test(raw)) return true;
|
|
106
|
+
if (FALSE_RX.test(raw)) return false;
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
function decodeOverrideConfigValue(raw) {
|
|
110
|
+
if (raw.startsWith("b64:")) {
|
|
111
|
+
try {
|
|
112
|
+
const json = atob(raw.slice(4).replace(/-/g, "+").replace(/_/g, "/"));
|
|
113
|
+
return JSON.parse(json);
|
|
114
|
+
} catch {
|
|
115
|
+
return raw;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(raw);
|
|
120
|
+
} catch {
|
|
121
|
+
return raw;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function parseOverrides(rawUrl) {
|
|
125
|
+
const gates = {};
|
|
126
|
+
const configs = {};
|
|
127
|
+
const experiments = {};
|
|
128
|
+
try {
|
|
129
|
+
const url = new URL(rawUrl, "http://localhost");
|
|
130
|
+
for (const [k, v] of url.searchParams) {
|
|
131
|
+
if (k.startsWith("se_ks_")) {
|
|
132
|
+
const b = parseOverrideBool(v);
|
|
133
|
+
if (b !== null) gates[k.slice(6)] = b;
|
|
134
|
+
} else if (k.startsWith("se_cf_")) {
|
|
135
|
+
configs[k.slice(6)] = decodeOverrideConfigValue(v);
|
|
136
|
+
} else if (k.startsWith("se_config_")) {
|
|
137
|
+
configs[k.slice(10)] = decodeOverrideConfigValue(v);
|
|
138
|
+
} else if (k.startsWith("se_exp_")) {
|
|
139
|
+
const name = k.slice(7);
|
|
140
|
+
if (v && v !== "default" && v !== "none") experiments[name] = v;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
return { gates, configs, experiments };
|
|
146
|
+
}
|
|
102
147
|
var FlagsClient = class {
|
|
103
148
|
apiKey;
|
|
104
149
|
baseUrl;
|
|
@@ -241,6 +286,38 @@ var FlagsClient = class {
|
|
|
241
286
|
body
|
|
242
287
|
}).catch((err) => console.warn("[shipeasy] track failed:", String(err)));
|
|
243
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* Evaluate all flags, configs, and experiments for a user against the locally
|
|
291
|
+
* cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
|
|
292
|
+
* overrides from the request URL when provided.
|
|
293
|
+
*
|
|
294
|
+
* Intended for SSR: call on the server, inject the result as
|
|
295
|
+
* `window.__SE_BOOTSTRAP` in the HTML, and the client SDK will read it
|
|
296
|
+
* synchronously without waiting for identify() to resolve.
|
|
297
|
+
*/
|
|
298
|
+
evaluate(user, rawUrl) {
|
|
299
|
+
const flags2 = {};
|
|
300
|
+
const configs = {};
|
|
301
|
+
const experiments = {};
|
|
302
|
+
for (const [name, gate] of Object.entries(this.flagsBlob?.gates ?? {})) {
|
|
303
|
+
flags2[name] = evalGateInternal(gate, user);
|
|
304
|
+
}
|
|
305
|
+
for (const [name, entry] of Object.entries(this.flagsBlob?.configs ?? {})) {
|
|
306
|
+
configs[name] = entry.value;
|
|
307
|
+
}
|
|
308
|
+
for (const [name] of Object.entries(this.expsBlob?.experiments ?? {})) {
|
|
309
|
+
experiments[name] = this.getExperiment(name, user, {});
|
|
310
|
+
}
|
|
311
|
+
if (rawUrl) {
|
|
312
|
+
const ov = parseOverrides(rawUrl);
|
|
313
|
+
Object.assign(flags2, ov.gates);
|
|
314
|
+
Object.assign(configs, ov.configs);
|
|
315
|
+
for (const [name, group] of Object.entries(ov.experiments)) {
|
|
316
|
+
experiments[name] = { inExperiment: true, group, params: {} };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return { flags: flags2, configs, experiments };
|
|
320
|
+
}
|
|
244
321
|
};
|
|
245
322
|
var DEFAULT_I18N_CDN = "https://cdn.i18n.shipeasy.ai";
|
|
246
323
|
async function fetchJson(url, timeoutMs = 2e3) {
|
|
@@ -321,6 +398,14 @@ var flags = {
|
|
|
321
398
|
},
|
|
322
399
|
track(userId, eventName, props) {
|
|
323
400
|
_server?.track(userId, eventName, props);
|
|
401
|
+
},
|
|
402
|
+
/**
|
|
403
|
+
* Evaluate all flags / configs / experiments for a user against the locally
|
|
404
|
+
* cached blob. Pass the request URL to apply ?se_ks_* / ?se_cf_* / ?se_exp_*
|
|
405
|
+
* overrides. Returns an empty payload when the blob hasn't been fetched yet.
|
|
406
|
+
*/
|
|
407
|
+
evaluate(user, rawUrl) {
|
|
408
|
+
return _server?.evaluate(user, rawUrl) ?? { flags: {}, configs: {}, experiments: {} };
|
|
324
409
|
}
|
|
325
410
|
};
|
|
326
411
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipeasy/sdk",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"description": "Shipeasy SDK — feature gates, runtime configs, experiments, and metrics for the Shipeasy hosted service.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://shipeasy.ai",
|