@shipeasy/sdk 5.0.0 → 5.1.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 +158 -0
- package/dist/client/index.d.mts +84 -3
- package/dist/client/index.d.ts +84 -3
- package/dist/client/index.js +138 -19
- package/dist/client/index.mjs +137 -19
- package/dist/server/index.d.mts +141 -4
- package/dist/server/index.d.ts +141 -4
- package/dist/server/index.js +193 -24
- package/dist/server/index.mjs +198 -24
- package/package.json +1 -1
package/dist/client/index.mjs
CHANGED
|
@@ -310,6 +310,14 @@ var SeeLimiter = class {
|
|
|
310
310
|
|
|
311
311
|
// src/client/index.ts
|
|
312
312
|
var version = "4.0.0";
|
|
313
|
+
var FLAG_REASONS = [
|
|
314
|
+
"CLIENT_NOT_READY",
|
|
315
|
+
"FLAG_NOT_FOUND",
|
|
316
|
+
"OFF",
|
|
317
|
+
"OVERRIDE",
|
|
318
|
+
"RULE_MATCH",
|
|
319
|
+
"DEFAULT"
|
|
320
|
+
];
|
|
313
321
|
var FLUSH_INTERVAL_MS = 5e3;
|
|
314
322
|
var MAX_BUFFER = 100;
|
|
315
323
|
var ANON_ID_KEY = "__se_anon_id";
|
|
@@ -821,7 +829,7 @@ function readExperimentOverridesFromUrl() {
|
|
|
821
829
|
}
|
|
822
830
|
return out;
|
|
823
831
|
}
|
|
824
|
-
var FlagsClientBrowser = class {
|
|
832
|
+
var FlagsClientBrowser = class _FlagsClientBrowser {
|
|
825
833
|
sdkKey;
|
|
826
834
|
baseUrl;
|
|
827
835
|
autoGuardrails;
|
|
@@ -840,6 +848,16 @@ var FlagsClientBrowser = class {
|
|
|
840
848
|
// Monotonic counter so a later identify() always wins even if its /sdk/evaluate
|
|
841
849
|
// response races and lands before an earlier in-flight call's response.
|
|
842
850
|
identifySeq = 0;
|
|
851
|
+
// Test mode: built by `FlagsClientBrowser.forTesting()`. When set, identify()
|
|
852
|
+
// never fetches, track() is a no-op, telemetry is off, and the client is
|
|
853
|
+
// already ready with an empty eval result.
|
|
854
|
+
testMode;
|
|
855
|
+
// Programmatic overrides (Statsig-style). Set on any client via
|
|
856
|
+
// overrideFlag/overrideConfig/overrideExperiment; they win over BOTH the URL
|
|
857
|
+
// overrides and the fetched eval result. Cleared by clearOverrides().
|
|
858
|
+
flagOverrides = /* @__PURE__ */ new Map();
|
|
859
|
+
configOverrides = /* @__PURE__ */ new Map();
|
|
860
|
+
experimentOverrides = /* @__PURE__ */ new Map();
|
|
843
861
|
onOverrideChange = () => {
|
|
844
862
|
this.installBridge();
|
|
845
863
|
this.notify();
|
|
@@ -848,6 +866,7 @@ var FlagsClientBrowser = class {
|
|
|
848
866
|
this.sdkKey = opts.sdkKey;
|
|
849
867
|
this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
|
|
850
868
|
this.env = opts.env ?? "prod";
|
|
869
|
+
this.testMode = opts.testMode === true;
|
|
851
870
|
this.autoGuardrails = opts.autoGuardrails !== false;
|
|
852
871
|
this.autoCollectAlways = opts.autoCollectAlways === true;
|
|
853
872
|
const g = opts.autoGuardrailGroups ?? {};
|
|
@@ -863,11 +882,42 @@ var FlagsClientBrowser = class {
|
|
|
863
882
|
sdkKey: this.sdkKey,
|
|
864
883
|
side: "client",
|
|
865
884
|
env: this.env,
|
|
866
|
-
|
|
885
|
+
// Test mode never talks to the network — telemetry off regardless of opt.
|
|
886
|
+
disabled: this.testMode || opts.disableTelemetry
|
|
887
|
+
});
|
|
888
|
+
if (this.testMode) {
|
|
889
|
+
this.evalResult = { flags: {}, configs: {}, experiments: {}, killswitches: {} };
|
|
890
|
+
} else {
|
|
891
|
+
void this.buffer.flushPendingAlias();
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Build a no-network, immediately-usable browser client for tests
|
|
896
|
+
* (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
|
|
897
|
+
* is a no-op, telemetry is disabled, and the client is already ready — seed
|
|
898
|
+
* every entity with overrideFlag/overrideConfig/overrideExperiment. No SDK
|
|
899
|
+
* key required.
|
|
900
|
+
*
|
|
901
|
+
* ```ts
|
|
902
|
+
* const client = FlagsClientBrowser.forTesting();
|
|
903
|
+
* client.overrideFlag("new_checkout", true);
|
|
904
|
+
* client.getFlag("new_checkout"); // true
|
|
905
|
+
* ```
|
|
906
|
+
*/
|
|
907
|
+
static forTesting(opts) {
|
|
908
|
+
return new _FlagsClientBrowser({
|
|
909
|
+
sdkKey: "",
|
|
910
|
+
autoGuardrails: false,
|
|
911
|
+
...opts,
|
|
912
|
+
testMode: true
|
|
867
913
|
});
|
|
868
|
-
void this.buffer.flushPendingAlias();
|
|
869
914
|
}
|
|
870
915
|
async identify(user) {
|
|
916
|
+
if (this.testMode) {
|
|
917
|
+
if (user.user_id !== void 0) this.userId = user.user_id;
|
|
918
|
+
this.notify();
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
871
921
|
const seq = ++this.identifySeq;
|
|
872
922
|
const prevUserId = this.userId;
|
|
873
923
|
if (user.user_id !== void 0) this.userId = user.user_id;
|
|
@@ -942,22 +992,75 @@ var FlagsClientBrowser = class {
|
|
|
942
992
|
initFromBootstrap(data) {
|
|
943
993
|
this.evalResult = data;
|
|
944
994
|
}
|
|
945
|
-
|
|
995
|
+
// ---- Local overrides (Statsig-style) ----
|
|
996
|
+
//
|
|
997
|
+
// Precedence (highest first): programmatic override (these methods) > URL
|
|
998
|
+
// override (?se_gate_/?se_config_/?se_exp_) > fetched eval result. A
|
|
999
|
+
// programmatic override is an explicit in-code decision, so it wins over the
|
|
1000
|
+
// ad-hoc URL/devtools overrides as well as the server's evaluation.
|
|
1001
|
+
/** Force `getFlag(name)` to return `value`, ignoring URL overrides + eval. */
|
|
1002
|
+
overrideFlag(name, value) {
|
|
1003
|
+
this.flagOverrides.set(name, value);
|
|
1004
|
+
}
|
|
1005
|
+
/** Force `getConfig(name)` to return `value`, ignoring URL overrides + eval. */
|
|
1006
|
+
overrideConfig(name, value) {
|
|
1007
|
+
this.configOverrides.set(name, value);
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Force `getExperiment(name, …)` to return `{ inExperiment: true, group, params }`,
|
|
1011
|
+
* ignoring URL overrides, the eval result, and exposure logging.
|
|
1012
|
+
*/
|
|
1013
|
+
overrideExperiment(name, group, params) {
|
|
1014
|
+
this.experimentOverrides.set(name, { group, params });
|
|
1015
|
+
}
|
|
1016
|
+
/** Remove every programmatic override set via the override* methods. */
|
|
1017
|
+
clearOverrides() {
|
|
1018
|
+
this.flagOverrides.clear();
|
|
1019
|
+
this.configOverrides.clear();
|
|
1020
|
+
this.experimentOverrides.clear();
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Evaluate a gate and report WHY (LaunchDarkly variationDetail parity). A
|
|
1024
|
+
* local override OR a ?se_gate_/?se_ks_ URL override short-circuits BEFORE
|
|
1025
|
+
* telemetry; otherwise exactly one "gate" beacon is emitted. The server
|
|
1026
|
+
* pre-evaluates a gate's enabled/killed state into a plain boolean, so OFF
|
|
1027
|
+
* folds into DEFAULT here (the browser can't tell "disabled" from "rule
|
|
1028
|
+
* denied").
|
|
1029
|
+
*/
|
|
1030
|
+
getFlagDetail(name) {
|
|
1031
|
+
const pov = this.flagOverrides.get(name);
|
|
1032
|
+
if (pov !== void 0) return { value: pov, reason: "OVERRIDE" };
|
|
1033
|
+
const urlOv = readGateOverride(name);
|
|
1034
|
+
if (urlOv !== null) return { value: urlOv, reason: "OVERRIDE" };
|
|
946
1035
|
this.telemetry.emit("gate", name);
|
|
947
|
-
if (this.evalResult === null) return false;
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
return
|
|
1036
|
+
if (this.evalResult === null) return { value: false, reason: "CLIENT_NOT_READY" };
|
|
1037
|
+
if (!(name in this.evalResult.flags)) return { value: false, reason: "FLAG_NOT_FOUND" };
|
|
1038
|
+
const value = this.evalResult.flags[name] ?? false;
|
|
1039
|
+
return { value, reason: value ? "RULE_MATCH" : "DEFAULT" };
|
|
951
1040
|
}
|
|
952
|
-
|
|
1041
|
+
/**
|
|
1042
|
+
* Read a feature gate. Returns `defaultValue` ONLY when the gate cannot be
|
|
1043
|
+
* evaluated (not ready or flag not found) — never for a gate that legitimately
|
|
1044
|
+
* evaluates to false. Plain `getFlag(name)` keeps returning false for a
|
|
1045
|
+
* missing flag.
|
|
1046
|
+
*/
|
|
1047
|
+
getFlag(name, defaultValue = false) {
|
|
1048
|
+
const d = this.getFlagDetail(name);
|
|
1049
|
+
if (d.reason === "CLIENT_NOT_READY" || d.reason === "FLAG_NOT_FOUND") return defaultValue;
|
|
1050
|
+
return d.value;
|
|
1051
|
+
}
|
|
1052
|
+
getConfig(name, decodeOrOpts) {
|
|
953
1053
|
this.telemetry.emit("config", name);
|
|
954
|
-
|
|
955
|
-
const
|
|
956
|
-
const
|
|
957
|
-
if (
|
|
958
|
-
|
|
1054
|
+
const opts = typeof decodeOrOpts === "function" ? { decode: decodeOrOpts } : decodeOrOpts ?? {};
|
|
1055
|
+
const fallback = "defaultValue" in opts ? opts.defaultValue : void 0;
|
|
1056
|
+
const hasProgrammatic = this.configOverrides.has(name);
|
|
1057
|
+
if (!hasProgrammatic && this.evalResult === null) return fallback;
|
|
1058
|
+
const urlOv = hasProgrammatic ? void 0 : readConfigOverride(name);
|
|
1059
|
+
const raw = hasProgrammatic ? this.configOverrides.get(name) : urlOv !== void 0 ? urlOv : this.evalResult?.configs?.[name];
|
|
1060
|
+
if (raw === void 0) return fallback;
|
|
1061
|
+
if (!opts.decode) return raw;
|
|
959
1062
|
try {
|
|
960
|
-
return decode(raw);
|
|
1063
|
+
return opts.decode(raw);
|
|
961
1064
|
} catch (err) {
|
|
962
1065
|
console.warn(`[shipeasy] getConfig('${name}') decode failed:`, String(err));
|
|
963
1066
|
return void 0;
|
|
@@ -970,6 +1073,12 @@ var FlagsClientBrowser = class {
|
|
|
970
1073
|
group: "control",
|
|
971
1074
|
params: defaultParams
|
|
972
1075
|
};
|
|
1076
|
+
const pov = this.experimentOverrides.get(name);
|
|
1077
|
+
if (pov) {
|
|
1078
|
+
const variantParams = variants?.[pov.group];
|
|
1079
|
+
const params = { ...defaultParams, ...pov.params, ...variantParams ?? {} };
|
|
1080
|
+
return { inExperiment: true, group: pov.group, params };
|
|
1081
|
+
}
|
|
973
1082
|
const ov = readExpOverride(name);
|
|
974
1083
|
if (ov !== null) {
|
|
975
1084
|
const variantParams = variants?.[ov];
|
|
@@ -1021,6 +1130,7 @@ var FlagsClientBrowser = class {
|
|
|
1021
1130
|
return bridge;
|
|
1022
1131
|
}
|
|
1023
1132
|
track(eventName, props) {
|
|
1133
|
+
if (this.testMode) return;
|
|
1024
1134
|
this.buffer.pushMetric(eventName, this.userId, this.anonId, props);
|
|
1025
1135
|
}
|
|
1026
1136
|
/**
|
|
@@ -1239,12 +1349,19 @@ var flags = {
|
|
|
1239
1349
|
* Everything else gates on _mountedAndReady to prevent hydration mismatches on
|
|
1240
1350
|
* force-static pages where SSR has no flag data.
|
|
1241
1351
|
*/
|
|
1242
|
-
get(name) {
|
|
1352
|
+
get(name, defaultValue = false) {
|
|
1243
1353
|
const bs = getBootstrap();
|
|
1244
1354
|
if (bs !== null && name in bs.flags) return bs.flags[name];
|
|
1245
|
-
if (!_mountedAndReady) return
|
|
1246
|
-
if (_client) return _client.getFlag(name);
|
|
1247
|
-
return readGateOverride(name) ??
|
|
1355
|
+
if (!_mountedAndReady) return defaultValue;
|
|
1356
|
+
if (_client) return _client.getFlag(name, defaultValue);
|
|
1357
|
+
return readGateOverride(name) ?? defaultValue;
|
|
1358
|
+
},
|
|
1359
|
+
/** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
|
|
1360
|
+
getDetail(name) {
|
|
1361
|
+
if (_client) return _client.getFlagDetail(name);
|
|
1362
|
+
const ov = readGateOverride(name);
|
|
1363
|
+
if (ov !== null) return { value: ov, reason: "OVERRIDE" };
|
|
1364
|
+
return { value: false, reason: "CLIENT_NOT_READY" };
|
|
1248
1365
|
},
|
|
1249
1366
|
getConfig(name, decode) {
|
|
1250
1367
|
const bs = getBootstrap();
|
|
@@ -1575,6 +1692,7 @@ var i18n = {
|
|
|
1575
1692
|
}
|
|
1576
1693
|
};
|
|
1577
1694
|
export {
|
|
1695
|
+
FLAG_REASONS,
|
|
1578
1696
|
FlagsClientBrowser,
|
|
1579
1697
|
LABEL_MARKER_END,
|
|
1580
1698
|
LABEL_MARKER_RE,
|
package/dist/server/index.d.mts
CHANGED
|
@@ -104,6 +104,14 @@ interface SeeControlFlowChain {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
declare const version = "4.0.0";
|
|
107
|
+
/**
|
|
108
|
+
* @internal Exported ONLY so the cross-language eval-parity golden-vector test
|
|
109
|
+
* (src/__tests__/eval-vectors.test.ts) can assert the raw unsigned-32-bit hash
|
|
110
|
+
* against the canonical fixture. Not part of the public SDK surface — do not
|
|
111
|
+
* rely on it in product code; the bucketing contract lives behind getFlag /
|
|
112
|
+
* getExperiment. Name-prefixed with `_` to signal "test seam, not API".
|
|
113
|
+
*/
|
|
114
|
+
declare function _murmur3ForTests(key: string): number;
|
|
107
115
|
interface User {
|
|
108
116
|
user_id?: string;
|
|
109
117
|
anonymous_id?: string;
|
|
@@ -114,6 +122,30 @@ interface ExperimentResult<P> {
|
|
|
114
122
|
group: string;
|
|
115
123
|
params: P;
|
|
116
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Why a flag evaluated the way it did (LaunchDarkly variationDetail parity).
|
|
127
|
+
* Computed at the client boundary, never inside the canonical eval:
|
|
128
|
+
* - CLIENT_NOT_READY — no rules blob loaded yet (init()/initOnce() pending)
|
|
129
|
+
* - FLAG_NOT_FOUND — the gate name isn't present in the loaded blob
|
|
130
|
+
* - OFF — the gate exists but is disabled / killed
|
|
131
|
+
* - OVERRIDE — a local override (overrideFlag) decided the value
|
|
132
|
+
* - RULE_MATCH — the gate evaluated true (rules + rollout passed)
|
|
133
|
+
* - DEFAULT — the gate evaluated false (a rule or the rollout denied)
|
|
134
|
+
*/
|
|
135
|
+
declare const FLAG_REASONS: readonly ["CLIENT_NOT_READY", "FLAG_NOT_FOUND", "OFF", "OVERRIDE", "RULE_MATCH", "DEFAULT"];
|
|
136
|
+
type FlagReason = (typeof FLAG_REASONS)[number];
|
|
137
|
+
interface FlagDetail {
|
|
138
|
+
value: boolean;
|
|
139
|
+
reason: FlagReason;
|
|
140
|
+
}
|
|
141
|
+
/** Options object form of `getConfig` — keeps the legacy `decode` callback and
|
|
142
|
+
* adds a `defaultValue` returned when the config key is absent. */
|
|
143
|
+
interface GetConfigOptions<T = unknown> {
|
|
144
|
+
/** Decode the raw stored value into the typed shape callers want. */
|
|
145
|
+
decode?: (raw: unknown) => T;
|
|
146
|
+
/** Returned when the config key is absent (not overridden, not in the blob). */
|
|
147
|
+
defaultValue?: T;
|
|
148
|
+
}
|
|
117
149
|
interface GateRule {
|
|
118
150
|
attr: string;
|
|
119
151
|
op: "eq" | "neq" | "in" | "not_in" | "gt" | "gte" | "lt" | "lte" | "contains" | "regex";
|
|
@@ -126,10 +158,27 @@ interface Gate {
|
|
|
126
158
|
enabled: 0 | 1 | boolean;
|
|
127
159
|
killswitch?: 0 | 1 | boolean;
|
|
128
160
|
}
|
|
161
|
+
interface ExperimentGroup {
|
|
162
|
+
name: string;
|
|
163
|
+
weight: number;
|
|
164
|
+
params: Record<string, unknown>;
|
|
165
|
+
}
|
|
166
|
+
interface Experiment {
|
|
167
|
+
universe: string;
|
|
168
|
+
targetingGate?: string | null;
|
|
169
|
+
allocationPct: number;
|
|
170
|
+
salt: string;
|
|
171
|
+
groups: ExperimentGroup[];
|
|
172
|
+
status: "draft" | "running" | "stopped" | "archived";
|
|
173
|
+
}
|
|
174
|
+
interface Universe {
|
|
175
|
+
holdout_range: [number, number] | null;
|
|
176
|
+
}
|
|
129
177
|
interface Killswitch {
|
|
130
178
|
killed: 0 | 1 | boolean;
|
|
131
179
|
switches?: Record<string, 0 | 1 | boolean>;
|
|
132
180
|
}
|
|
181
|
+
/** Body of `GET /sdk/flags` — the snapshot's `flags` field. See {@link FlagsClient.fromSnapshot}. */
|
|
133
182
|
interface FlagsBlob {
|
|
134
183
|
version: string;
|
|
135
184
|
plan: string;
|
|
@@ -139,6 +188,12 @@ interface FlagsBlob {
|
|
|
139
188
|
}>;
|
|
140
189
|
killswitches: Record<string, Killswitch>;
|
|
141
190
|
}
|
|
191
|
+
/** Body of `GET /sdk/experiments` — the snapshot's `experiments` field. See {@link FlagsClient.fromSnapshot}. */
|
|
192
|
+
interface ExpsBlob {
|
|
193
|
+
version: string;
|
|
194
|
+
universes: Record<string, Universe>;
|
|
195
|
+
experiments: Record<string, Experiment>;
|
|
196
|
+
}
|
|
142
197
|
interface BootstrapPayload {
|
|
143
198
|
flags: Record<string, boolean>;
|
|
144
199
|
configs: Record<string, unknown>;
|
|
@@ -168,6 +223,13 @@ interface FlagsClientOptions {
|
|
|
168
223
|
disableTelemetry?: boolean;
|
|
169
224
|
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
170
225
|
telemetryUrl?: string;
|
|
226
|
+
/**
|
|
227
|
+
* Test mode — no network at all. init()/initOnce() are no-ops (never fetch),
|
|
228
|
+
* track() is a no-op, telemetry is forced off, and the client starts
|
|
229
|
+
* "initialized" with an empty blob. Prefer the {@link FlagsClient.forTesting}
|
|
230
|
+
* factory over passing this directly.
|
|
231
|
+
*/
|
|
232
|
+
testMode?: boolean;
|
|
171
233
|
}
|
|
172
234
|
declare class FlagsClient {
|
|
173
235
|
private readonly apiKey;
|
|
@@ -182,16 +244,89 @@ declare class FlagsClient {
|
|
|
182
244
|
private pollInterval;
|
|
183
245
|
private timer;
|
|
184
246
|
private initialized;
|
|
247
|
+
private readonly testMode;
|
|
248
|
+
private readonly flagOverrides;
|
|
249
|
+
private readonly configOverrides;
|
|
250
|
+
private readonly experimentOverrides;
|
|
251
|
+
private readonly changeListeners;
|
|
185
252
|
constructor(opts: FlagsClientOptions);
|
|
253
|
+
/**
|
|
254
|
+
* Build a no-network, immediately-usable client for tests (Statsig-style).
|
|
255
|
+
* init()/initOnce() are no-ops (never fetch), track() is a no-op, telemetry
|
|
256
|
+
* is disabled, and the client is already "initialized" — seed every entity
|
|
257
|
+
* with overrideFlag/overrideConfig/overrideExperiment. No SDK key required.
|
|
258
|
+
*
|
|
259
|
+
* ```ts
|
|
260
|
+
* const client = FlagsClient.forTesting();
|
|
261
|
+
* client.overrideFlag("new_checkout", true);
|
|
262
|
+
* client.getFlag("new_checkout", { user_id: "u1" }); // true
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
static forTesting(opts?: Partial<FlagsClientOptions>): FlagsClient;
|
|
266
|
+
/**
|
|
267
|
+
* Build a fully OFFLINE client from a pre-captured snapshot — no network ever.
|
|
268
|
+
* Reuses the test-mode plumbing (init()/initOnce()/track() are no-ops,
|
|
269
|
+
* telemetry off) but, unlike forTesting(), seeds the REAL flags + experiments
|
|
270
|
+
* blobs so evaluations run the canonical eval against the snapshot. Local
|
|
271
|
+
* overrides still apply on top.
|
|
272
|
+
*
|
|
273
|
+
* Snapshot shape mirrors the wire bodies:
|
|
274
|
+
* `{ flags: <GET /sdk/flags body>, experiments: <GET /sdk/experiments body> }`.
|
|
275
|
+
*/
|
|
276
|
+
static fromSnapshot(snapshot: {
|
|
277
|
+
flags: FlagsBlob;
|
|
278
|
+
experiments: ExpsBlob;
|
|
279
|
+
}): FlagsClient;
|
|
280
|
+
/**
|
|
281
|
+
* Build a fully OFFLINE client from a snapshot JSON file on disk (Node only —
|
|
282
|
+
* not available in the browser entrypoint). The file must contain
|
|
283
|
+
* `{ "flags": <GET /sdk/flags body>, "experiments": <GET /sdk/experiments body> }`.
|
|
284
|
+
* See {@link FlagsClient.fromSnapshot}.
|
|
285
|
+
*/
|
|
286
|
+
static fromFile(path: string): FlagsClient;
|
|
186
287
|
init(): Promise<void>;
|
|
187
288
|
initOnce(): Promise<void>;
|
|
289
|
+
/** Force `getFlag(name, …)` to return `value`, ignoring the fetched gate. */
|
|
290
|
+
overrideFlag(name: string, value: boolean): void;
|
|
291
|
+
/** Force `getConfig(name)` to return `value`, ignoring the fetched config. */
|
|
292
|
+
overrideConfig(name: string, value: unknown): void;
|
|
293
|
+
/**
|
|
294
|
+
* Force `getExperiment(name, …)` to return `{ inExperiment: true, group, params }`,
|
|
295
|
+
* ignoring allocation, holdouts, and targeting.
|
|
296
|
+
*/
|
|
297
|
+
overrideExperiment(name: string, group: string, params: Record<string, unknown>): void;
|
|
298
|
+
/** Remove every programmatic override set via the override* methods. */
|
|
299
|
+
clearOverrides(): void;
|
|
188
300
|
destroy(): void;
|
|
301
|
+
/**
|
|
302
|
+
* Subscribe to data-change notifications. The listener fires after a
|
|
303
|
+
* background poll fetch returns NEW data (200, not 304) — i.e. after the
|
|
304
|
+
* cached blob is updated. Never fires in testMode/offline (no polling).
|
|
305
|
+
* Returns an unsubscribe function.
|
|
306
|
+
*/
|
|
307
|
+
onChange(listener: () => void): () => void;
|
|
308
|
+
private notifyChange;
|
|
189
309
|
private startPoll;
|
|
190
310
|
private fetchAll;
|
|
191
311
|
private fetchFlags;
|
|
192
312
|
private fetchExps;
|
|
193
|
-
|
|
313
|
+
/**
|
|
314
|
+
* Evaluate a gate and report WHY (LaunchDarkly variationDetail parity). The
|
|
315
|
+
* reason is computed entirely at this boundary — the canonical eval
|
|
316
|
+
* (evalGateInternal) is untouched. A local override short-circuits BEFORE
|
|
317
|
+
* telemetry, exactly like getFlag's override path; otherwise exactly one
|
|
318
|
+
* "gate" beacon is emitted.
|
|
319
|
+
*/
|
|
320
|
+
getFlagDetail(name: string, user: User): FlagDetail;
|
|
321
|
+
/**
|
|
322
|
+
* Read a feature gate. Returns `defaultValue` ONLY when the gate cannot be
|
|
323
|
+
* evaluated (client not initialized or flag not found) — never for a gate that
|
|
324
|
+
* legitimately evaluates to false. Plain `getFlag(name, user)` keeps returning
|
|
325
|
+
* false for a missing flag.
|
|
326
|
+
*/
|
|
327
|
+
getFlag(name: string, user: User, defaultValue?: boolean): boolean;
|
|
194
328
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
329
|
+
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
195
330
|
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
196
331
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
197
332
|
/**
|
|
@@ -338,8 +473,10 @@ declare const flags: {
|
|
|
338
473
|
initOnce(): Promise<void>;
|
|
339
474
|
/** Stop background timers. Safe to call repeatedly. */
|
|
340
475
|
destroy(): void;
|
|
341
|
-
get(name: string, user: User): boolean;
|
|
342
|
-
|
|
476
|
+
get(name: string, user: User, defaultValue?: boolean): boolean;
|
|
477
|
+
/** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
|
|
478
|
+
getDetail(name: string, user: User): FlagDetail;
|
|
479
|
+
getConfig<T = unknown>(name: string, decodeOrOpts?: ((raw: unknown) => T) | GetConfigOptions<T>): T | undefined;
|
|
343
480
|
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
344
481
|
/**
|
|
345
482
|
* Read a killswitch. Without `switchKey`, returns true when the whole
|
|
@@ -406,4 +543,4 @@ interface SeeApi {
|
|
|
406
543
|
*/
|
|
407
544
|
declare const see: SeeApi;
|
|
408
545
|
|
|
409
|
-
export { ANON_ID_COOKIE, type BootstrapHtmlOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type I18nForRequest, type LabelFile, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, type Violation, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
|
|
546
|
+
export { ANON_ID_COOKIE, type BootstrapHtmlOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type ExpsBlob, FLAG_REASONS, type FetchLabelsOptions, type FlagDetail, type FlagReason, type FlagsBlob, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type GetConfigOptions, type I18nForRequest, type LabelFile, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, type Violation, _murmur3ForTests, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -104,6 +104,14 @@ interface SeeControlFlowChain {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
declare const version = "4.0.0";
|
|
107
|
+
/**
|
|
108
|
+
* @internal Exported ONLY so the cross-language eval-parity golden-vector test
|
|
109
|
+
* (src/__tests__/eval-vectors.test.ts) can assert the raw unsigned-32-bit hash
|
|
110
|
+
* against the canonical fixture. Not part of the public SDK surface — do not
|
|
111
|
+
* rely on it in product code; the bucketing contract lives behind getFlag /
|
|
112
|
+
* getExperiment. Name-prefixed with `_` to signal "test seam, not API".
|
|
113
|
+
*/
|
|
114
|
+
declare function _murmur3ForTests(key: string): number;
|
|
107
115
|
interface User {
|
|
108
116
|
user_id?: string;
|
|
109
117
|
anonymous_id?: string;
|
|
@@ -114,6 +122,30 @@ interface ExperimentResult<P> {
|
|
|
114
122
|
group: string;
|
|
115
123
|
params: P;
|
|
116
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Why a flag evaluated the way it did (LaunchDarkly variationDetail parity).
|
|
127
|
+
* Computed at the client boundary, never inside the canonical eval:
|
|
128
|
+
* - CLIENT_NOT_READY — no rules blob loaded yet (init()/initOnce() pending)
|
|
129
|
+
* - FLAG_NOT_FOUND — the gate name isn't present in the loaded blob
|
|
130
|
+
* - OFF — the gate exists but is disabled / killed
|
|
131
|
+
* - OVERRIDE — a local override (overrideFlag) decided the value
|
|
132
|
+
* - RULE_MATCH — the gate evaluated true (rules + rollout passed)
|
|
133
|
+
* - DEFAULT — the gate evaluated false (a rule or the rollout denied)
|
|
134
|
+
*/
|
|
135
|
+
declare const FLAG_REASONS: readonly ["CLIENT_NOT_READY", "FLAG_NOT_FOUND", "OFF", "OVERRIDE", "RULE_MATCH", "DEFAULT"];
|
|
136
|
+
type FlagReason = (typeof FLAG_REASONS)[number];
|
|
137
|
+
interface FlagDetail {
|
|
138
|
+
value: boolean;
|
|
139
|
+
reason: FlagReason;
|
|
140
|
+
}
|
|
141
|
+
/** Options object form of `getConfig` — keeps the legacy `decode` callback and
|
|
142
|
+
* adds a `defaultValue` returned when the config key is absent. */
|
|
143
|
+
interface GetConfigOptions<T = unknown> {
|
|
144
|
+
/** Decode the raw stored value into the typed shape callers want. */
|
|
145
|
+
decode?: (raw: unknown) => T;
|
|
146
|
+
/** Returned when the config key is absent (not overridden, not in the blob). */
|
|
147
|
+
defaultValue?: T;
|
|
148
|
+
}
|
|
117
149
|
interface GateRule {
|
|
118
150
|
attr: string;
|
|
119
151
|
op: "eq" | "neq" | "in" | "not_in" | "gt" | "gte" | "lt" | "lte" | "contains" | "regex";
|
|
@@ -126,10 +158,27 @@ interface Gate {
|
|
|
126
158
|
enabled: 0 | 1 | boolean;
|
|
127
159
|
killswitch?: 0 | 1 | boolean;
|
|
128
160
|
}
|
|
161
|
+
interface ExperimentGroup {
|
|
162
|
+
name: string;
|
|
163
|
+
weight: number;
|
|
164
|
+
params: Record<string, unknown>;
|
|
165
|
+
}
|
|
166
|
+
interface Experiment {
|
|
167
|
+
universe: string;
|
|
168
|
+
targetingGate?: string | null;
|
|
169
|
+
allocationPct: number;
|
|
170
|
+
salt: string;
|
|
171
|
+
groups: ExperimentGroup[];
|
|
172
|
+
status: "draft" | "running" | "stopped" | "archived";
|
|
173
|
+
}
|
|
174
|
+
interface Universe {
|
|
175
|
+
holdout_range: [number, number] | null;
|
|
176
|
+
}
|
|
129
177
|
interface Killswitch {
|
|
130
178
|
killed: 0 | 1 | boolean;
|
|
131
179
|
switches?: Record<string, 0 | 1 | boolean>;
|
|
132
180
|
}
|
|
181
|
+
/** Body of `GET /sdk/flags` — the snapshot's `flags` field. See {@link FlagsClient.fromSnapshot}. */
|
|
133
182
|
interface FlagsBlob {
|
|
134
183
|
version: string;
|
|
135
184
|
plan: string;
|
|
@@ -139,6 +188,12 @@ interface FlagsBlob {
|
|
|
139
188
|
}>;
|
|
140
189
|
killswitches: Record<string, Killswitch>;
|
|
141
190
|
}
|
|
191
|
+
/** Body of `GET /sdk/experiments` — the snapshot's `experiments` field. See {@link FlagsClient.fromSnapshot}. */
|
|
192
|
+
interface ExpsBlob {
|
|
193
|
+
version: string;
|
|
194
|
+
universes: Record<string, Universe>;
|
|
195
|
+
experiments: Record<string, Experiment>;
|
|
196
|
+
}
|
|
142
197
|
interface BootstrapPayload {
|
|
143
198
|
flags: Record<string, boolean>;
|
|
144
199
|
configs: Record<string, unknown>;
|
|
@@ -168,6 +223,13 @@ interface FlagsClientOptions {
|
|
|
168
223
|
disableTelemetry?: boolean;
|
|
169
224
|
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
170
225
|
telemetryUrl?: string;
|
|
226
|
+
/**
|
|
227
|
+
* Test mode — no network at all. init()/initOnce() are no-ops (never fetch),
|
|
228
|
+
* track() is a no-op, telemetry is forced off, and the client starts
|
|
229
|
+
* "initialized" with an empty blob. Prefer the {@link FlagsClient.forTesting}
|
|
230
|
+
* factory over passing this directly.
|
|
231
|
+
*/
|
|
232
|
+
testMode?: boolean;
|
|
171
233
|
}
|
|
172
234
|
declare class FlagsClient {
|
|
173
235
|
private readonly apiKey;
|
|
@@ -182,16 +244,89 @@ declare class FlagsClient {
|
|
|
182
244
|
private pollInterval;
|
|
183
245
|
private timer;
|
|
184
246
|
private initialized;
|
|
247
|
+
private readonly testMode;
|
|
248
|
+
private readonly flagOverrides;
|
|
249
|
+
private readonly configOverrides;
|
|
250
|
+
private readonly experimentOverrides;
|
|
251
|
+
private readonly changeListeners;
|
|
185
252
|
constructor(opts: FlagsClientOptions);
|
|
253
|
+
/**
|
|
254
|
+
* Build a no-network, immediately-usable client for tests (Statsig-style).
|
|
255
|
+
* init()/initOnce() are no-ops (never fetch), track() is a no-op, telemetry
|
|
256
|
+
* is disabled, and the client is already "initialized" — seed every entity
|
|
257
|
+
* with overrideFlag/overrideConfig/overrideExperiment. No SDK key required.
|
|
258
|
+
*
|
|
259
|
+
* ```ts
|
|
260
|
+
* const client = FlagsClient.forTesting();
|
|
261
|
+
* client.overrideFlag("new_checkout", true);
|
|
262
|
+
* client.getFlag("new_checkout", { user_id: "u1" }); // true
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
static forTesting(opts?: Partial<FlagsClientOptions>): FlagsClient;
|
|
266
|
+
/**
|
|
267
|
+
* Build a fully OFFLINE client from a pre-captured snapshot — no network ever.
|
|
268
|
+
* Reuses the test-mode plumbing (init()/initOnce()/track() are no-ops,
|
|
269
|
+
* telemetry off) but, unlike forTesting(), seeds the REAL flags + experiments
|
|
270
|
+
* blobs so evaluations run the canonical eval against the snapshot. Local
|
|
271
|
+
* overrides still apply on top.
|
|
272
|
+
*
|
|
273
|
+
* Snapshot shape mirrors the wire bodies:
|
|
274
|
+
* `{ flags: <GET /sdk/flags body>, experiments: <GET /sdk/experiments body> }`.
|
|
275
|
+
*/
|
|
276
|
+
static fromSnapshot(snapshot: {
|
|
277
|
+
flags: FlagsBlob;
|
|
278
|
+
experiments: ExpsBlob;
|
|
279
|
+
}): FlagsClient;
|
|
280
|
+
/**
|
|
281
|
+
* Build a fully OFFLINE client from a snapshot JSON file on disk (Node only —
|
|
282
|
+
* not available in the browser entrypoint). The file must contain
|
|
283
|
+
* `{ "flags": <GET /sdk/flags body>, "experiments": <GET /sdk/experiments body> }`.
|
|
284
|
+
* See {@link FlagsClient.fromSnapshot}.
|
|
285
|
+
*/
|
|
286
|
+
static fromFile(path: string): FlagsClient;
|
|
186
287
|
init(): Promise<void>;
|
|
187
288
|
initOnce(): Promise<void>;
|
|
289
|
+
/** Force `getFlag(name, …)` to return `value`, ignoring the fetched gate. */
|
|
290
|
+
overrideFlag(name: string, value: boolean): void;
|
|
291
|
+
/** Force `getConfig(name)` to return `value`, ignoring the fetched config. */
|
|
292
|
+
overrideConfig(name: string, value: unknown): void;
|
|
293
|
+
/**
|
|
294
|
+
* Force `getExperiment(name, …)` to return `{ inExperiment: true, group, params }`,
|
|
295
|
+
* ignoring allocation, holdouts, and targeting.
|
|
296
|
+
*/
|
|
297
|
+
overrideExperiment(name: string, group: string, params: Record<string, unknown>): void;
|
|
298
|
+
/** Remove every programmatic override set via the override* methods. */
|
|
299
|
+
clearOverrides(): void;
|
|
188
300
|
destroy(): void;
|
|
301
|
+
/**
|
|
302
|
+
* Subscribe to data-change notifications. The listener fires after a
|
|
303
|
+
* background poll fetch returns NEW data (200, not 304) — i.e. after the
|
|
304
|
+
* cached blob is updated. Never fires in testMode/offline (no polling).
|
|
305
|
+
* Returns an unsubscribe function.
|
|
306
|
+
*/
|
|
307
|
+
onChange(listener: () => void): () => void;
|
|
308
|
+
private notifyChange;
|
|
189
309
|
private startPoll;
|
|
190
310
|
private fetchAll;
|
|
191
311
|
private fetchFlags;
|
|
192
312
|
private fetchExps;
|
|
193
|
-
|
|
313
|
+
/**
|
|
314
|
+
* Evaluate a gate and report WHY (LaunchDarkly variationDetail parity). The
|
|
315
|
+
* reason is computed entirely at this boundary — the canonical eval
|
|
316
|
+
* (evalGateInternal) is untouched. A local override short-circuits BEFORE
|
|
317
|
+
* telemetry, exactly like getFlag's override path; otherwise exactly one
|
|
318
|
+
* "gate" beacon is emitted.
|
|
319
|
+
*/
|
|
320
|
+
getFlagDetail(name: string, user: User): FlagDetail;
|
|
321
|
+
/**
|
|
322
|
+
* Read a feature gate. Returns `defaultValue` ONLY when the gate cannot be
|
|
323
|
+
* evaluated (client not initialized or flag not found) — never for a gate that
|
|
324
|
+
* legitimately evaluates to false. Plain `getFlag(name, user)` keeps returning
|
|
325
|
+
* false for a missing flag.
|
|
326
|
+
*/
|
|
327
|
+
getFlag(name: string, user: User, defaultValue?: boolean): boolean;
|
|
194
328
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
329
|
+
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
195
330
|
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
196
331
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
197
332
|
/**
|
|
@@ -338,8 +473,10 @@ declare const flags: {
|
|
|
338
473
|
initOnce(): Promise<void>;
|
|
339
474
|
/** Stop background timers. Safe to call repeatedly. */
|
|
340
475
|
destroy(): void;
|
|
341
|
-
get(name: string, user: User): boolean;
|
|
342
|
-
|
|
476
|
+
get(name: string, user: User, defaultValue?: boolean): boolean;
|
|
477
|
+
/** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
|
|
478
|
+
getDetail(name: string, user: User): FlagDetail;
|
|
479
|
+
getConfig<T = unknown>(name: string, decodeOrOpts?: ((raw: unknown) => T) | GetConfigOptions<T>): T | undefined;
|
|
343
480
|
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
344
481
|
/**
|
|
345
482
|
* Read a killswitch. Without `switchKey`, returns true when the whole
|
|
@@ -406,4 +543,4 @@ interface SeeApi {
|
|
|
406
543
|
*/
|
|
407
544
|
declare const see: SeeApi;
|
|
408
545
|
|
|
409
|
-
export { ANON_ID_COOKIE, type BootstrapHtmlOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type I18nForRequest, type LabelFile, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, type Violation, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
|
|
546
|
+
export { ANON_ID_COOKIE, type BootstrapHtmlOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type ExpsBlob, FLAG_REASONS, type FetchLabelsOptions, type FlagDetail, type FlagReason, type FlagsBlob, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type GetConfigOptions, type I18nForRequest, type LabelFile, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, type Violation, _murmur3ForTests, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
|