@shipeasy/sdk 2.0.1 → 2.0.2
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 +15 -3
- package/dist/client/index.d.ts +15 -3
- package/dist/client/index.js +34 -11
- package/dist/client/index.mjs +34 -11
- 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: URL override → server bootstrap (window.__SE_BOOTSTRAP) → CDN-fetched (post-mount) → false.
|
|
141
|
+
* The _mountedAndReady gate still applies for the CDN path to prevent hydration
|
|
142
|
+
* mismatches on force-static pages; bootstrap data is safe to read immediately
|
|
143
|
+
* because the server rendered with the same values.
|
|
132
144
|
*/
|
|
133
145
|
get(name: string): boolean;
|
|
134
146
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
@@ -190,4 +202,4 @@ declare const i18n: {
|
|
|
190
202
|
onUpdate(cb: () => void): () => void;
|
|
191
203
|
};
|
|
192
204
|
|
|
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 };
|
|
205
|
+
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: URL override → server bootstrap (window.__SE_BOOTSTRAP) → CDN-fetched (post-mount) → false.
|
|
141
|
+
* The _mountedAndReady gate still applies for the CDN path to prevent hydration
|
|
142
|
+
* mismatches on force-static pages; bootstrap data is safe to read immediately
|
|
143
|
+
* because the server rendered with the same values.
|
|
132
144
|
*/
|
|
133
145
|
get(name: string): boolean;
|
|
134
146
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
@@ -190,4 +202,4 @@ declare const i18n: {
|
|
|
190
202
|
onUpdate(cb: () => void): () => void;
|
|
191
203
|
};
|
|
192
204
|
|
|
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 };
|
|
205
|
+
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,25 +685,44 @@ 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: URL override → server bootstrap (window.__SE_BOOTSTRAP) → CDN-fetched (post-mount) → false.
|
|
690
|
+
* The _mountedAndReady gate still applies for the CDN path to prevent hydration
|
|
691
|
+
* mismatches on force-static pages; bootstrap data is safe to read immediately
|
|
692
|
+
* because the server rendered with the same values.
|
|
686
693
|
*/
|
|
687
694
|
get(name) {
|
|
695
|
+
const ov = readGateOverride(name);
|
|
696
|
+
if (ov !== null) return ov;
|
|
697
|
+
const bs = getBootstrap();
|
|
698
|
+
if (bs !== null && name in bs.flags) return bs.flags[name];
|
|
688
699
|
if (!_mountedAndReady) return false;
|
|
689
700
|
if (_client) return _client.getFlag(name);
|
|
690
|
-
return
|
|
701
|
+
return false;
|
|
691
702
|
},
|
|
692
703
|
getConfig(name, decode) {
|
|
693
|
-
if (!_mountedAndReady) return void 0;
|
|
694
|
-
if (_client) return _client.getConfig(name, decode);
|
|
695
704
|
const ov = readConfigOverride(name);
|
|
696
|
-
if (ov
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
705
|
+
if (ov !== void 0) {
|
|
706
|
+
if (!decode) return ov;
|
|
707
|
+
try {
|
|
708
|
+
return decode(ov);
|
|
709
|
+
} catch {
|
|
710
|
+
return void 0;
|
|
711
|
+
}
|
|
702
712
|
}
|
|
713
|
+
const bs = getBootstrap();
|
|
714
|
+
if (bs !== null && name in bs.configs) {
|
|
715
|
+
const raw = bs.configs[name];
|
|
716
|
+
if (!decode) return raw;
|
|
717
|
+
try {
|
|
718
|
+
return decode(raw);
|
|
719
|
+
} catch {
|
|
720
|
+
return void 0;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (!_mountedAndReady) return void 0;
|
|
724
|
+
if (_client) return _client.getConfig(name, decode);
|
|
725
|
+
return void 0;
|
|
703
726
|
},
|
|
704
727
|
getExperiment(name, defaultParams, decode, variants) {
|
|
705
728
|
return _client?.getExperiment(name, defaultParams, decode, variants) ?? {
|
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,25 +643,44 @@ 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: URL override → server bootstrap (window.__SE_BOOTSTRAP) → CDN-fetched (post-mount) → false.
|
|
648
|
+
* The _mountedAndReady gate still applies for the CDN path to prevent hydration
|
|
649
|
+
* mismatches on force-static pages; bootstrap data is safe to read immediately
|
|
650
|
+
* because the server rendered with the same values.
|
|
644
651
|
*/
|
|
645
652
|
get(name) {
|
|
653
|
+
const ov = readGateOverride(name);
|
|
654
|
+
if (ov !== null) return ov;
|
|
655
|
+
const bs = getBootstrap();
|
|
656
|
+
if (bs !== null && name in bs.flags) return bs.flags[name];
|
|
646
657
|
if (!_mountedAndReady) return false;
|
|
647
658
|
if (_client) return _client.getFlag(name);
|
|
648
|
-
return
|
|
659
|
+
return false;
|
|
649
660
|
},
|
|
650
661
|
getConfig(name, decode) {
|
|
651
|
-
if (!_mountedAndReady) return void 0;
|
|
652
|
-
if (_client) return _client.getConfig(name, decode);
|
|
653
662
|
const ov = readConfigOverride(name);
|
|
654
|
-
if (ov
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
663
|
+
if (ov !== void 0) {
|
|
664
|
+
if (!decode) return ov;
|
|
665
|
+
try {
|
|
666
|
+
return decode(ov);
|
|
667
|
+
} catch {
|
|
668
|
+
return void 0;
|
|
669
|
+
}
|
|
660
670
|
}
|
|
671
|
+
const bs = getBootstrap();
|
|
672
|
+
if (bs !== null && name in bs.configs) {
|
|
673
|
+
const raw = bs.configs[name];
|
|
674
|
+
if (!decode) return raw;
|
|
675
|
+
try {
|
|
676
|
+
return decode(raw);
|
|
677
|
+
} catch {
|
|
678
|
+
return void 0;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (!_mountedAndReady) return void 0;
|
|
682
|
+
if (_client) return _client.getConfig(name, decode);
|
|
683
|
+
return void 0;
|
|
661
684
|
},
|
|
662
685
|
getExperiment(name, defaultParams, decode, variants) {
|
|
663
686
|
return _client?.getExperiment(name, defaultParams, decode, variants) ?? {
|
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.2",
|
|
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",
|