@shipeasy/sdk 5.0.0 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -0
- package/dist/client/index.d.mts +169 -4
- package/dist/client/index.d.ts +169 -4
- package/dist/client/index.js +228 -26
- package/dist/client/index.mjs +227 -26
- package/dist/openfeature-server/index.d.mts +338 -0
- package/dist/openfeature-server/index.d.ts +338 -0
- package/dist/openfeature-server/index.js +125 -0
- package/dist/openfeature-server/index.mjs +100 -0
- package/dist/openfeature-web/index.d.mts +332 -0
- package/dist/openfeature-web/index.d.ts +332 -0
- package/dist/openfeature-web/index.js +120 -0
- package/dist/openfeature-web/index.mjs +95 -0
- package/dist/server/index.d.mts +198 -4
- package/dist/server/index.d.ts +198 -4
- package/dist/server/index.js +290 -36
- package/dist/server/index.mjs +294 -36
- package/package.json +23 -1
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,47 @@ 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
|
+
/** Attribute to bucket on (e.g. company_id); defaults to user_id/anonymous_id. */
|
|
174
|
+
bucketBy?: string | null;
|
|
175
|
+
}
|
|
176
|
+
/** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
|
|
177
|
+
interface StickyEntry {
|
|
178
|
+
g: string;
|
|
179
|
+
s: string;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Pluggable sticky-bucketing store for the server (doc 20 §2). Keyed by the
|
|
183
|
+
* bucketing unit; the value is that unit's per-experiment assignments. Absent
|
|
184
|
+
* from {@link FlagsClientOptions} ⇒ today's deterministic behaviour. Use
|
|
185
|
+
* {@link createInMemoryStickyStore} or a cookie-bridge built from request
|
|
186
|
+
* cookies.
|
|
187
|
+
*/
|
|
188
|
+
interface StickyBucketStore {
|
|
189
|
+
get(unit: string): Record<string, StickyEntry> | undefined;
|
|
190
|
+
set(unit: string, exp: string, entry: StickyEntry): void;
|
|
191
|
+
}
|
|
192
|
+
/** A process-local sticky store (Map-backed). Handy for tests + single-process servers. */
|
|
193
|
+
declare function createInMemoryStickyStore(seed?: Record<string, Record<string, StickyEntry>>): StickyBucketStore;
|
|
194
|
+
interface Universe {
|
|
195
|
+
holdout_range: [number, number] | null;
|
|
196
|
+
}
|
|
129
197
|
interface Killswitch {
|
|
130
198
|
killed: 0 | 1 | boolean;
|
|
131
199
|
switches?: Record<string, 0 | 1 | boolean>;
|
|
132
200
|
}
|
|
201
|
+
/** Body of `GET /sdk/flags` — the snapshot's `flags` field. See {@link FlagsClient.fromSnapshot}. */
|
|
133
202
|
interface FlagsBlob {
|
|
134
203
|
version: string;
|
|
135
204
|
plan: string;
|
|
@@ -139,6 +208,12 @@ interface FlagsBlob {
|
|
|
139
208
|
}>;
|
|
140
209
|
killswitches: Record<string, Killswitch>;
|
|
141
210
|
}
|
|
211
|
+
/** Body of `GET /sdk/experiments` — the snapshot's `experiments` field. See {@link FlagsClient.fromSnapshot}. */
|
|
212
|
+
interface ExpsBlob {
|
|
213
|
+
version: string;
|
|
214
|
+
universes: Record<string, Universe>;
|
|
215
|
+
experiments: Record<string, Experiment>;
|
|
216
|
+
}
|
|
142
217
|
interface BootstrapPayload {
|
|
143
218
|
flags: Record<string, boolean>;
|
|
144
219
|
configs: Record<string, unknown>;
|
|
@@ -168,11 +243,35 @@ interface FlagsClientOptions {
|
|
|
168
243
|
disableTelemetry?: boolean;
|
|
169
244
|
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
170
245
|
telemetryUrl?: string;
|
|
246
|
+
/**
|
|
247
|
+
* Attribute names usable for targeting but never persisted in analytics
|
|
248
|
+
* (LD/Statsig `privateAttributes`). The server evaluates locally so private
|
|
249
|
+
* attrs never leave for evaluation at all; the only egress is `/collect`, and
|
|
250
|
+
* the listed keys are stripped from every outbound `track()` payload.
|
|
251
|
+
*/
|
|
252
|
+
privateAttributes?: string[];
|
|
253
|
+
/**
|
|
254
|
+
* Sticky-bucketing store (doc 20 §2). When provided, `getExperiment` locks a
|
|
255
|
+
* unit to its first-assigned variant — changing allocation % or weights won't
|
|
256
|
+
* re-bucket enrolled units (changing the experiment salt is the reshuffle
|
|
257
|
+
* lever). Absent ⇒ deterministic (fully backward compatible). Built-ins:
|
|
258
|
+
* {@link createInMemoryStickyStore}, or a cookie-bridge over `__se_sticky`.
|
|
259
|
+
*/
|
|
260
|
+
stickyStore?: StickyBucketStore;
|
|
261
|
+
/**
|
|
262
|
+
* Test mode — no network at all. init()/initOnce() are no-ops (never fetch),
|
|
263
|
+
* track() is a no-op, telemetry is forced off, and the client starts
|
|
264
|
+
* "initialized" with an empty blob. Prefer the {@link FlagsClient.forTesting}
|
|
265
|
+
* factory over passing this directly.
|
|
266
|
+
*/
|
|
267
|
+
testMode?: boolean;
|
|
171
268
|
}
|
|
172
269
|
declare class FlagsClient {
|
|
173
270
|
private readonly apiKey;
|
|
174
271
|
private readonly baseUrl;
|
|
175
272
|
private readonly env;
|
|
273
|
+
private readonly privateAttributes;
|
|
274
|
+
private readonly stickyStore;
|
|
176
275
|
private readonly telemetry;
|
|
177
276
|
private readonly seeLimiter;
|
|
178
277
|
private flagsBlob;
|
|
@@ -182,18 +281,102 @@ declare class FlagsClient {
|
|
|
182
281
|
private pollInterval;
|
|
183
282
|
private timer;
|
|
184
283
|
private initialized;
|
|
284
|
+
private readonly testMode;
|
|
285
|
+
private readonly flagOverrides;
|
|
286
|
+
private readonly configOverrides;
|
|
287
|
+
private readonly experimentOverrides;
|
|
288
|
+
private readonly changeListeners;
|
|
185
289
|
constructor(opts: FlagsClientOptions);
|
|
290
|
+
/**
|
|
291
|
+
* Build a no-network, immediately-usable client for tests (Statsig-style).
|
|
292
|
+
* init()/initOnce() are no-ops (never fetch), track() is a no-op, telemetry
|
|
293
|
+
* is disabled, and the client is already "initialized" — seed every entity
|
|
294
|
+
* with overrideFlag/overrideConfig/overrideExperiment. No SDK key required.
|
|
295
|
+
*
|
|
296
|
+
* ```ts
|
|
297
|
+
* const client = FlagsClient.forTesting();
|
|
298
|
+
* client.overrideFlag("new_checkout", true);
|
|
299
|
+
* client.getFlag("new_checkout", { user_id: "u1" }); // true
|
|
300
|
+
* ```
|
|
301
|
+
*/
|
|
302
|
+
static forTesting(opts?: Partial<FlagsClientOptions>): FlagsClient;
|
|
303
|
+
/**
|
|
304
|
+
* Build a fully OFFLINE client from a pre-captured snapshot — no network ever.
|
|
305
|
+
* Reuses the test-mode plumbing (init()/initOnce()/track() are no-ops,
|
|
306
|
+
* telemetry off) but, unlike forTesting(), seeds the REAL flags + experiments
|
|
307
|
+
* blobs so evaluations run the canonical eval against the snapshot. Local
|
|
308
|
+
* overrides still apply on top.
|
|
309
|
+
*
|
|
310
|
+
* Snapshot shape mirrors the wire bodies:
|
|
311
|
+
* `{ flags: <GET /sdk/flags body>, experiments: <GET /sdk/experiments body> }`.
|
|
312
|
+
*/
|
|
313
|
+
static fromSnapshot(snapshot: {
|
|
314
|
+
flags: FlagsBlob;
|
|
315
|
+
experiments: ExpsBlob;
|
|
316
|
+
}): FlagsClient;
|
|
317
|
+
/**
|
|
318
|
+
* Build a fully OFFLINE client from a snapshot JSON file on disk (Node only —
|
|
319
|
+
* not available in the browser entrypoint). The file must contain
|
|
320
|
+
* `{ "flags": <GET /sdk/flags body>, "experiments": <GET /sdk/experiments body> }`.
|
|
321
|
+
* See {@link FlagsClient.fromSnapshot}.
|
|
322
|
+
*/
|
|
323
|
+
static fromFile(path: string): FlagsClient;
|
|
186
324
|
init(): Promise<void>;
|
|
187
325
|
initOnce(): Promise<void>;
|
|
326
|
+
/** Force `getFlag(name, …)` to return `value`, ignoring the fetched gate. */
|
|
327
|
+
overrideFlag(name: string, value: boolean): void;
|
|
328
|
+
/** Force `getConfig(name)` to return `value`, ignoring the fetched config. */
|
|
329
|
+
overrideConfig(name: string, value: unknown): void;
|
|
330
|
+
/**
|
|
331
|
+
* Force `getExperiment(name, …)` to return `{ inExperiment: true, group, params }`,
|
|
332
|
+
* ignoring allocation, holdouts, and targeting.
|
|
333
|
+
*/
|
|
334
|
+
overrideExperiment(name: string, group: string, params: Record<string, unknown>): void;
|
|
335
|
+
/** Remove every programmatic override set via the override* methods. */
|
|
336
|
+
clearOverrides(): void;
|
|
188
337
|
destroy(): void;
|
|
338
|
+
/**
|
|
339
|
+
* Subscribe to data-change notifications. The listener fires after a
|
|
340
|
+
* background poll fetch returns NEW data (200, not 304) — i.e. after the
|
|
341
|
+
* cached blob is updated. Never fires in testMode/offline (no polling).
|
|
342
|
+
* Returns an unsubscribe function.
|
|
343
|
+
*/
|
|
344
|
+
onChange(listener: () => void): () => void;
|
|
345
|
+
private notifyChange;
|
|
189
346
|
private startPoll;
|
|
190
347
|
private fetchAll;
|
|
191
348
|
private fetchFlags;
|
|
192
349
|
private fetchExps;
|
|
193
|
-
|
|
350
|
+
/**
|
|
351
|
+
* Evaluate a gate and report WHY (LaunchDarkly variationDetail parity). The
|
|
352
|
+
* reason is computed entirely at this boundary — the canonical eval
|
|
353
|
+
* (evalGateInternal) is untouched. A local override short-circuits BEFORE
|
|
354
|
+
* telemetry, exactly like getFlag's override path; otherwise exactly one
|
|
355
|
+
* "gate" beacon is emitted.
|
|
356
|
+
*/
|
|
357
|
+
getFlagDetail(name: string, user: User): FlagDetail;
|
|
358
|
+
/**
|
|
359
|
+
* Read a feature gate. Returns `defaultValue` ONLY when the gate cannot be
|
|
360
|
+
* evaluated (client not initialized or flag not found) — never for a gate that
|
|
361
|
+
* legitimately evaluates to false. Plain `getFlag(name, user)` keeps returning
|
|
362
|
+
* false for a missing flag.
|
|
363
|
+
*/
|
|
364
|
+
getFlag(name: string, user: User, defaultValue?: boolean): boolean;
|
|
194
365
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
366
|
+
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
195
367
|
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
368
|
+
/** Drop caller-marked private attributes from an outbound props bag. */
|
|
369
|
+
private stripPrivate;
|
|
196
370
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
371
|
+
/**
|
|
372
|
+
* Emit an exposure event for an experiment at the server-side decision point
|
|
373
|
+
* (parity with the browser's auto-exposure). The server is stateless and
|
|
374
|
+
* never auto-logs, so call this when you actually present the treatment.
|
|
375
|
+
* Re-evaluates the experiment for `user` (a bare `user_id` string is wrapped
|
|
376
|
+
* as `{ user_id }`); if the user is enrolled, POSTs a single exposure to
|
|
377
|
+
* `/collect`. No-op in test mode or when the user isn't enrolled.
|
|
378
|
+
*/
|
|
379
|
+
logExposure(user: string | User, name: string): void;
|
|
197
380
|
/**
|
|
198
381
|
* Report a structured error into the errors primitive. Fire-and-forget —
|
|
199
382
|
* never blocks or throws into the request path. Spam-guarded by a 30s
|
|
@@ -283,6 +466,12 @@ interface ShipeasyServerConfig {
|
|
|
283
466
|
* that evaluate many flags per request. See {@link FlagsClientOptions.disableTelemetry}.
|
|
284
467
|
*/
|
|
285
468
|
disableTelemetry?: boolean;
|
|
469
|
+
/**
|
|
470
|
+
* Attribute names usable for targeting but never persisted in analytics
|
|
471
|
+
* (LD/Statsig `privateAttributes`). Stripped from every outbound `track()`
|
|
472
|
+
* payload. See {@link FlagsClientOptions.privateAttributes}.
|
|
473
|
+
*/
|
|
474
|
+
privateAttributes?: string[];
|
|
286
475
|
}
|
|
287
476
|
interface ShipeasyServerHandle {
|
|
288
477
|
flags: Record<string, boolean>;
|
|
@@ -338,8 +527,10 @@ declare const flags: {
|
|
|
338
527
|
initOnce(): Promise<void>;
|
|
339
528
|
/** Stop background timers. Safe to call repeatedly. */
|
|
340
529
|
destroy(): void;
|
|
341
|
-
get(name: string, user: User): boolean;
|
|
342
|
-
|
|
530
|
+
get(name: string, user: User, defaultValue?: boolean): boolean;
|
|
531
|
+
/** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
|
|
532
|
+
getDetail(name: string, user: User): FlagDetail;
|
|
533
|
+
getConfig<T = unknown>(name: string, decodeOrOpts?: ((raw: unknown) => T) | GetConfigOptions<T>): T | undefined;
|
|
343
534
|
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
344
535
|
/**
|
|
345
536
|
* Read a killswitch. Without `switchKey`, returns true when the whole
|
|
@@ -348,6 +539,9 @@ declare const flags: {
|
|
|
348
539
|
*/
|
|
349
540
|
ks(name: string, switchKey?: string): boolean;
|
|
350
541
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
542
|
+
/** Emit an exposure for an enrolled experiment at the decision point. See
|
|
543
|
+
* {@link FlagsClient.logExposure}. No-op before configure(). */
|
|
544
|
+
logExposure(user: string | User, name: string): void;
|
|
351
545
|
/**
|
|
352
546
|
* Evaluate all flags / configs / experiments for a user against the locally
|
|
353
547
|
* cached blob. Pass the request URL to apply ?se_ks_* / ?se_cf_* / ?se_exp_*
|
|
@@ -406,4 +600,4 @@ interface SeeApi {
|
|
|
406
600
|
*/
|
|
407
601
|
declare const see: SeeApi;
|
|
408
602
|
|
|
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 };
|
|
603
|
+
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 StickyBucketStore, type StickyEntry, type User, type Violation, _murmur3ForTests, _resetShipeasyServerForTests, configureShipeasyServer, createInMemoryStickyStore, 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,47 @@ 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
|
+
/** Attribute to bucket on (e.g. company_id); defaults to user_id/anonymous_id. */
|
|
174
|
+
bucketBy?: string | null;
|
|
175
|
+
}
|
|
176
|
+
/** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
|
|
177
|
+
interface StickyEntry {
|
|
178
|
+
g: string;
|
|
179
|
+
s: string;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Pluggable sticky-bucketing store for the server (doc 20 §2). Keyed by the
|
|
183
|
+
* bucketing unit; the value is that unit's per-experiment assignments. Absent
|
|
184
|
+
* from {@link FlagsClientOptions} ⇒ today's deterministic behaviour. Use
|
|
185
|
+
* {@link createInMemoryStickyStore} or a cookie-bridge built from request
|
|
186
|
+
* cookies.
|
|
187
|
+
*/
|
|
188
|
+
interface StickyBucketStore {
|
|
189
|
+
get(unit: string): Record<string, StickyEntry> | undefined;
|
|
190
|
+
set(unit: string, exp: string, entry: StickyEntry): void;
|
|
191
|
+
}
|
|
192
|
+
/** A process-local sticky store (Map-backed). Handy for tests + single-process servers. */
|
|
193
|
+
declare function createInMemoryStickyStore(seed?: Record<string, Record<string, StickyEntry>>): StickyBucketStore;
|
|
194
|
+
interface Universe {
|
|
195
|
+
holdout_range: [number, number] | null;
|
|
196
|
+
}
|
|
129
197
|
interface Killswitch {
|
|
130
198
|
killed: 0 | 1 | boolean;
|
|
131
199
|
switches?: Record<string, 0 | 1 | boolean>;
|
|
132
200
|
}
|
|
201
|
+
/** Body of `GET /sdk/flags` — the snapshot's `flags` field. See {@link FlagsClient.fromSnapshot}. */
|
|
133
202
|
interface FlagsBlob {
|
|
134
203
|
version: string;
|
|
135
204
|
plan: string;
|
|
@@ -139,6 +208,12 @@ interface FlagsBlob {
|
|
|
139
208
|
}>;
|
|
140
209
|
killswitches: Record<string, Killswitch>;
|
|
141
210
|
}
|
|
211
|
+
/** Body of `GET /sdk/experiments` — the snapshot's `experiments` field. See {@link FlagsClient.fromSnapshot}. */
|
|
212
|
+
interface ExpsBlob {
|
|
213
|
+
version: string;
|
|
214
|
+
universes: Record<string, Universe>;
|
|
215
|
+
experiments: Record<string, Experiment>;
|
|
216
|
+
}
|
|
142
217
|
interface BootstrapPayload {
|
|
143
218
|
flags: Record<string, boolean>;
|
|
144
219
|
configs: Record<string, unknown>;
|
|
@@ -168,11 +243,35 @@ interface FlagsClientOptions {
|
|
|
168
243
|
disableTelemetry?: boolean;
|
|
169
244
|
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
170
245
|
telemetryUrl?: string;
|
|
246
|
+
/**
|
|
247
|
+
* Attribute names usable for targeting but never persisted in analytics
|
|
248
|
+
* (LD/Statsig `privateAttributes`). The server evaluates locally so private
|
|
249
|
+
* attrs never leave for evaluation at all; the only egress is `/collect`, and
|
|
250
|
+
* the listed keys are stripped from every outbound `track()` payload.
|
|
251
|
+
*/
|
|
252
|
+
privateAttributes?: string[];
|
|
253
|
+
/**
|
|
254
|
+
* Sticky-bucketing store (doc 20 §2). When provided, `getExperiment` locks a
|
|
255
|
+
* unit to its first-assigned variant — changing allocation % or weights won't
|
|
256
|
+
* re-bucket enrolled units (changing the experiment salt is the reshuffle
|
|
257
|
+
* lever). Absent ⇒ deterministic (fully backward compatible). Built-ins:
|
|
258
|
+
* {@link createInMemoryStickyStore}, or a cookie-bridge over `__se_sticky`.
|
|
259
|
+
*/
|
|
260
|
+
stickyStore?: StickyBucketStore;
|
|
261
|
+
/**
|
|
262
|
+
* Test mode — no network at all. init()/initOnce() are no-ops (never fetch),
|
|
263
|
+
* track() is a no-op, telemetry is forced off, and the client starts
|
|
264
|
+
* "initialized" with an empty blob. Prefer the {@link FlagsClient.forTesting}
|
|
265
|
+
* factory over passing this directly.
|
|
266
|
+
*/
|
|
267
|
+
testMode?: boolean;
|
|
171
268
|
}
|
|
172
269
|
declare class FlagsClient {
|
|
173
270
|
private readonly apiKey;
|
|
174
271
|
private readonly baseUrl;
|
|
175
272
|
private readonly env;
|
|
273
|
+
private readonly privateAttributes;
|
|
274
|
+
private readonly stickyStore;
|
|
176
275
|
private readonly telemetry;
|
|
177
276
|
private readonly seeLimiter;
|
|
178
277
|
private flagsBlob;
|
|
@@ -182,18 +281,102 @@ declare class FlagsClient {
|
|
|
182
281
|
private pollInterval;
|
|
183
282
|
private timer;
|
|
184
283
|
private initialized;
|
|
284
|
+
private readonly testMode;
|
|
285
|
+
private readonly flagOverrides;
|
|
286
|
+
private readonly configOverrides;
|
|
287
|
+
private readonly experimentOverrides;
|
|
288
|
+
private readonly changeListeners;
|
|
185
289
|
constructor(opts: FlagsClientOptions);
|
|
290
|
+
/**
|
|
291
|
+
* Build a no-network, immediately-usable client for tests (Statsig-style).
|
|
292
|
+
* init()/initOnce() are no-ops (never fetch), track() is a no-op, telemetry
|
|
293
|
+
* is disabled, and the client is already "initialized" — seed every entity
|
|
294
|
+
* with overrideFlag/overrideConfig/overrideExperiment. No SDK key required.
|
|
295
|
+
*
|
|
296
|
+
* ```ts
|
|
297
|
+
* const client = FlagsClient.forTesting();
|
|
298
|
+
* client.overrideFlag("new_checkout", true);
|
|
299
|
+
* client.getFlag("new_checkout", { user_id: "u1" }); // true
|
|
300
|
+
* ```
|
|
301
|
+
*/
|
|
302
|
+
static forTesting(opts?: Partial<FlagsClientOptions>): FlagsClient;
|
|
303
|
+
/**
|
|
304
|
+
* Build a fully OFFLINE client from a pre-captured snapshot — no network ever.
|
|
305
|
+
* Reuses the test-mode plumbing (init()/initOnce()/track() are no-ops,
|
|
306
|
+
* telemetry off) but, unlike forTesting(), seeds the REAL flags + experiments
|
|
307
|
+
* blobs so evaluations run the canonical eval against the snapshot. Local
|
|
308
|
+
* overrides still apply on top.
|
|
309
|
+
*
|
|
310
|
+
* Snapshot shape mirrors the wire bodies:
|
|
311
|
+
* `{ flags: <GET /sdk/flags body>, experiments: <GET /sdk/experiments body> }`.
|
|
312
|
+
*/
|
|
313
|
+
static fromSnapshot(snapshot: {
|
|
314
|
+
flags: FlagsBlob;
|
|
315
|
+
experiments: ExpsBlob;
|
|
316
|
+
}): FlagsClient;
|
|
317
|
+
/**
|
|
318
|
+
* Build a fully OFFLINE client from a snapshot JSON file on disk (Node only —
|
|
319
|
+
* not available in the browser entrypoint). The file must contain
|
|
320
|
+
* `{ "flags": <GET /sdk/flags body>, "experiments": <GET /sdk/experiments body> }`.
|
|
321
|
+
* See {@link FlagsClient.fromSnapshot}.
|
|
322
|
+
*/
|
|
323
|
+
static fromFile(path: string): FlagsClient;
|
|
186
324
|
init(): Promise<void>;
|
|
187
325
|
initOnce(): Promise<void>;
|
|
326
|
+
/** Force `getFlag(name, …)` to return `value`, ignoring the fetched gate. */
|
|
327
|
+
overrideFlag(name: string, value: boolean): void;
|
|
328
|
+
/** Force `getConfig(name)` to return `value`, ignoring the fetched config. */
|
|
329
|
+
overrideConfig(name: string, value: unknown): void;
|
|
330
|
+
/**
|
|
331
|
+
* Force `getExperiment(name, …)` to return `{ inExperiment: true, group, params }`,
|
|
332
|
+
* ignoring allocation, holdouts, and targeting.
|
|
333
|
+
*/
|
|
334
|
+
overrideExperiment(name: string, group: string, params: Record<string, unknown>): void;
|
|
335
|
+
/** Remove every programmatic override set via the override* methods. */
|
|
336
|
+
clearOverrides(): void;
|
|
188
337
|
destroy(): void;
|
|
338
|
+
/**
|
|
339
|
+
* Subscribe to data-change notifications. The listener fires after a
|
|
340
|
+
* background poll fetch returns NEW data (200, not 304) — i.e. after the
|
|
341
|
+
* cached blob is updated. Never fires in testMode/offline (no polling).
|
|
342
|
+
* Returns an unsubscribe function.
|
|
343
|
+
*/
|
|
344
|
+
onChange(listener: () => void): () => void;
|
|
345
|
+
private notifyChange;
|
|
189
346
|
private startPoll;
|
|
190
347
|
private fetchAll;
|
|
191
348
|
private fetchFlags;
|
|
192
349
|
private fetchExps;
|
|
193
|
-
|
|
350
|
+
/**
|
|
351
|
+
* Evaluate a gate and report WHY (LaunchDarkly variationDetail parity). The
|
|
352
|
+
* reason is computed entirely at this boundary — the canonical eval
|
|
353
|
+
* (evalGateInternal) is untouched. A local override short-circuits BEFORE
|
|
354
|
+
* telemetry, exactly like getFlag's override path; otherwise exactly one
|
|
355
|
+
* "gate" beacon is emitted.
|
|
356
|
+
*/
|
|
357
|
+
getFlagDetail(name: string, user: User): FlagDetail;
|
|
358
|
+
/**
|
|
359
|
+
* Read a feature gate. Returns `defaultValue` ONLY when the gate cannot be
|
|
360
|
+
* evaluated (client not initialized or flag not found) — never for a gate that
|
|
361
|
+
* legitimately evaluates to false. Plain `getFlag(name, user)` keeps returning
|
|
362
|
+
* false for a missing flag.
|
|
363
|
+
*/
|
|
364
|
+
getFlag(name: string, user: User, defaultValue?: boolean): boolean;
|
|
194
365
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
366
|
+
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
195
367
|
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
368
|
+
/** Drop caller-marked private attributes from an outbound props bag. */
|
|
369
|
+
private stripPrivate;
|
|
196
370
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
371
|
+
/**
|
|
372
|
+
* Emit an exposure event for an experiment at the server-side decision point
|
|
373
|
+
* (parity with the browser's auto-exposure). The server is stateless and
|
|
374
|
+
* never auto-logs, so call this when you actually present the treatment.
|
|
375
|
+
* Re-evaluates the experiment for `user` (a bare `user_id` string is wrapped
|
|
376
|
+
* as `{ user_id }`); if the user is enrolled, POSTs a single exposure to
|
|
377
|
+
* `/collect`. No-op in test mode or when the user isn't enrolled.
|
|
378
|
+
*/
|
|
379
|
+
logExposure(user: string | User, name: string): void;
|
|
197
380
|
/**
|
|
198
381
|
* Report a structured error into the errors primitive. Fire-and-forget —
|
|
199
382
|
* never blocks or throws into the request path. Spam-guarded by a 30s
|
|
@@ -283,6 +466,12 @@ interface ShipeasyServerConfig {
|
|
|
283
466
|
* that evaluate many flags per request. See {@link FlagsClientOptions.disableTelemetry}.
|
|
284
467
|
*/
|
|
285
468
|
disableTelemetry?: boolean;
|
|
469
|
+
/**
|
|
470
|
+
* Attribute names usable for targeting but never persisted in analytics
|
|
471
|
+
* (LD/Statsig `privateAttributes`). Stripped from every outbound `track()`
|
|
472
|
+
* payload. See {@link FlagsClientOptions.privateAttributes}.
|
|
473
|
+
*/
|
|
474
|
+
privateAttributes?: string[];
|
|
286
475
|
}
|
|
287
476
|
interface ShipeasyServerHandle {
|
|
288
477
|
flags: Record<string, boolean>;
|
|
@@ -338,8 +527,10 @@ declare const flags: {
|
|
|
338
527
|
initOnce(): Promise<void>;
|
|
339
528
|
/** Stop background timers. Safe to call repeatedly. */
|
|
340
529
|
destroy(): void;
|
|
341
|
-
get(name: string, user: User): boolean;
|
|
342
|
-
|
|
530
|
+
get(name: string, user: User, defaultValue?: boolean): boolean;
|
|
531
|
+
/** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
|
|
532
|
+
getDetail(name: string, user: User): FlagDetail;
|
|
533
|
+
getConfig<T = unknown>(name: string, decodeOrOpts?: ((raw: unknown) => T) | GetConfigOptions<T>): T | undefined;
|
|
343
534
|
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
344
535
|
/**
|
|
345
536
|
* Read a killswitch. Without `switchKey`, returns true when the whole
|
|
@@ -348,6 +539,9 @@ declare const flags: {
|
|
|
348
539
|
*/
|
|
349
540
|
ks(name: string, switchKey?: string): boolean;
|
|
350
541
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
542
|
+
/** Emit an exposure for an enrolled experiment at the decision point. See
|
|
543
|
+
* {@link FlagsClient.logExposure}. No-op before configure(). */
|
|
544
|
+
logExposure(user: string | User, name: string): void;
|
|
351
545
|
/**
|
|
352
546
|
* Evaluate all flags / configs / experiments for a user against the locally
|
|
353
547
|
* cached blob. Pass the request URL to apply ?se_ks_* / ?se_cf_* / ?se_exp_*
|
|
@@ -406,4 +600,4 @@ interface SeeApi {
|
|
|
406
600
|
*/
|
|
407
601
|
declare const see: SeeApi;
|
|
408
602
|
|
|
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 };
|
|
603
|
+
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 StickyBucketStore, type StickyEntry, type User, type Violation, _murmur3ForTests, _resetShipeasyServerForTests, configureShipeasyServer, createInMemoryStickyStore, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
|