@shipeasy/sdk 5.1.0 → 5.3.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/dist/client/index.d.mts +86 -2
- package/dist/client/index.d.ts +86 -2
- package/dist/client/index.js +90 -7
- package/dist/client/index.mjs +90 -7
- 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 +58 -1
- package/dist/server/index.d.ts +58 -1
- package/dist/server/index.js +97 -15
- package/dist/server/index.mjs +96 -15
- package/package.json +23 -1
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { Provider, EvaluationContext, Logger, ResolutionDetails, JsonValue } from '@openfeature/server-sdk';
|
|
2
|
+
|
|
3
|
+
type SeeExtras = Record<string, string | number | boolean | null | undefined>;
|
|
4
|
+
type SeeKind = "caught" | "uncaught" | "unhandled_rejection" | "network" | "violation";
|
|
5
|
+
/** Built by `causesThe(subject).to(outcome)` — never constructed by hand. */
|
|
6
|
+
interface Consequence {
|
|
7
|
+
readonly __seConsequence: true;
|
|
8
|
+
readonly subject: string;
|
|
9
|
+
readonly outcome: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface User {
|
|
13
|
+
user_id?: string;
|
|
14
|
+
anonymous_id?: string;
|
|
15
|
+
[attr: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
interface ExperimentResult<P> {
|
|
18
|
+
inExperiment: boolean;
|
|
19
|
+
group: string;
|
|
20
|
+
params: P;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Why a flag evaluated the way it did (LaunchDarkly variationDetail parity).
|
|
24
|
+
* Computed at the client boundary, never inside the canonical eval:
|
|
25
|
+
* - CLIENT_NOT_READY — no rules blob loaded yet (init()/initOnce() pending)
|
|
26
|
+
* - FLAG_NOT_FOUND — the gate name isn't present in the loaded blob
|
|
27
|
+
* - OFF — the gate exists but is disabled / killed
|
|
28
|
+
* - OVERRIDE — a local override (overrideFlag) decided the value
|
|
29
|
+
* - RULE_MATCH — the gate evaluated true (rules + rollout passed)
|
|
30
|
+
* - DEFAULT — the gate evaluated false (a rule or the rollout denied)
|
|
31
|
+
*/
|
|
32
|
+
declare const FLAG_REASONS: readonly ["CLIENT_NOT_READY", "FLAG_NOT_FOUND", "OFF", "OVERRIDE", "RULE_MATCH", "DEFAULT"];
|
|
33
|
+
type FlagReason = (typeof FLAG_REASONS)[number];
|
|
34
|
+
interface FlagDetail {
|
|
35
|
+
value: boolean;
|
|
36
|
+
reason: FlagReason;
|
|
37
|
+
}
|
|
38
|
+
/** Options object form of `getConfig` — keeps the legacy `decode` callback and
|
|
39
|
+
* adds a `defaultValue` returned when the config key is absent. */
|
|
40
|
+
interface GetConfigOptions<T = unknown> {
|
|
41
|
+
/** Decode the raw stored value into the typed shape callers want. */
|
|
42
|
+
decode?: (raw: unknown) => T;
|
|
43
|
+
/** Returned when the config key is absent (not overridden, not in the blob). */
|
|
44
|
+
defaultValue?: T;
|
|
45
|
+
}
|
|
46
|
+
interface GateRule {
|
|
47
|
+
attr: string;
|
|
48
|
+
op: "eq" | "neq" | "in" | "not_in" | "gt" | "gte" | "lt" | "lte" | "contains" | "regex";
|
|
49
|
+
value: unknown;
|
|
50
|
+
}
|
|
51
|
+
interface Gate {
|
|
52
|
+
rules: GateRule[];
|
|
53
|
+
rolloutPct: number;
|
|
54
|
+
salt: string;
|
|
55
|
+
enabled: 0 | 1 | boolean;
|
|
56
|
+
killswitch?: 0 | 1 | boolean;
|
|
57
|
+
}
|
|
58
|
+
interface ExperimentGroup {
|
|
59
|
+
name: string;
|
|
60
|
+
weight: number;
|
|
61
|
+
params: Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
interface Experiment {
|
|
64
|
+
universe: string;
|
|
65
|
+
targetingGate?: string | null;
|
|
66
|
+
allocationPct: number;
|
|
67
|
+
salt: string;
|
|
68
|
+
groups: ExperimentGroup[];
|
|
69
|
+
status: "draft" | "running" | "stopped" | "archived";
|
|
70
|
+
/** Attribute to bucket on (e.g. company_id); defaults to user_id/anonymous_id. */
|
|
71
|
+
bucketBy?: string | null;
|
|
72
|
+
}
|
|
73
|
+
/** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
|
|
74
|
+
interface StickyEntry {
|
|
75
|
+
g: string;
|
|
76
|
+
s: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Pluggable sticky-bucketing store for the server (doc 20 §2). Keyed by the
|
|
80
|
+
* bucketing unit; the value is that unit's per-experiment assignments. Absent
|
|
81
|
+
* from {@link FlagsClientOptions} ⇒ today's deterministic behaviour. Use
|
|
82
|
+
* {@link createInMemoryStickyStore} or a cookie-bridge built from request
|
|
83
|
+
* cookies.
|
|
84
|
+
*/
|
|
85
|
+
interface StickyBucketStore {
|
|
86
|
+
get(unit: string): Record<string, StickyEntry> | undefined;
|
|
87
|
+
set(unit: string, exp: string, entry: StickyEntry): void;
|
|
88
|
+
}
|
|
89
|
+
interface Universe {
|
|
90
|
+
holdout_range: [number, number] | null;
|
|
91
|
+
}
|
|
92
|
+
interface Killswitch {
|
|
93
|
+
killed: 0 | 1 | boolean;
|
|
94
|
+
switches?: Record<string, 0 | 1 | boolean>;
|
|
95
|
+
}
|
|
96
|
+
/** Body of `GET /sdk/flags` — the snapshot's `flags` field. See {@link FlagsClient.fromSnapshot}. */
|
|
97
|
+
interface FlagsBlob {
|
|
98
|
+
version: string;
|
|
99
|
+
plan: string;
|
|
100
|
+
gates: Record<string, Gate>;
|
|
101
|
+
configs: Record<string, {
|
|
102
|
+
value: unknown;
|
|
103
|
+
}>;
|
|
104
|
+
killswitches: Record<string, Killswitch>;
|
|
105
|
+
}
|
|
106
|
+
/** Body of `GET /sdk/experiments` — the snapshot's `experiments` field. See {@link FlagsClient.fromSnapshot}. */
|
|
107
|
+
interface ExpsBlob {
|
|
108
|
+
version: string;
|
|
109
|
+
universes: Record<string, Universe>;
|
|
110
|
+
experiments: Record<string, Experiment>;
|
|
111
|
+
}
|
|
112
|
+
interface BootstrapPayload {
|
|
113
|
+
flags: Record<string, boolean>;
|
|
114
|
+
configs: Record<string, unknown>;
|
|
115
|
+
experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
|
|
116
|
+
killswitches: Record<string, boolean | Record<string, boolean>>;
|
|
117
|
+
}
|
|
118
|
+
type FlagsClientEnv = "dev" | "staging" | "prod";
|
|
119
|
+
interface FlagsClientOptions {
|
|
120
|
+
apiKey: string;
|
|
121
|
+
baseUrl?: string;
|
|
122
|
+
/** Which published env to read values from. Defaults to "prod". */
|
|
123
|
+
env?: FlagsClientEnv;
|
|
124
|
+
/**
|
|
125
|
+
* Preload the flags blob synchronously without a network fetch. Primarily
|
|
126
|
+
* for tests; production callers should rely on init()/initOnce().
|
|
127
|
+
*/
|
|
128
|
+
initialBlob?: FlagsBlob;
|
|
129
|
+
/**
|
|
130
|
+
* Per-evaluation usage telemetry. ON by default — each getFlag/getConfig/
|
|
131
|
+
* getExperiment/getKillswitch (and the per-key evaluate() loop) fires one
|
|
132
|
+
* fire-and-forget beacon counted by Cloudflare's native per-path analytics.
|
|
133
|
+
* Pass `true` to disable. NOTE: on Cloudflare Workers each beacon is an
|
|
134
|
+
* outbound subrequest (cap 50 free / 1000 paid per invocation), so disable
|
|
135
|
+
* this on hot request paths that evaluate many flags per request.
|
|
136
|
+
*/
|
|
137
|
+
disableTelemetry?: boolean;
|
|
138
|
+
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
139
|
+
telemetryUrl?: string;
|
|
140
|
+
/**
|
|
141
|
+
* Attribute names usable for targeting but never persisted in analytics
|
|
142
|
+
* (LD/Statsig `privateAttributes`). The server evaluates locally so private
|
|
143
|
+
* attrs never leave for evaluation at all; the only egress is `/collect`, and
|
|
144
|
+
* the listed keys are stripped from every outbound `track()` payload.
|
|
145
|
+
*/
|
|
146
|
+
privateAttributes?: string[];
|
|
147
|
+
/**
|
|
148
|
+
* Sticky-bucketing store (doc 20 §2). When provided, `getExperiment` locks a
|
|
149
|
+
* unit to its first-assigned variant — changing allocation % or weights won't
|
|
150
|
+
* re-bucket enrolled units (changing the experiment salt is the reshuffle
|
|
151
|
+
* lever). Absent ⇒ deterministic (fully backward compatible). Built-ins:
|
|
152
|
+
* {@link createInMemoryStickyStore}, or a cookie-bridge over `__se_sticky`.
|
|
153
|
+
*/
|
|
154
|
+
stickyStore?: StickyBucketStore;
|
|
155
|
+
/**
|
|
156
|
+
* Test mode — no network at all. init()/initOnce() are no-ops (never fetch),
|
|
157
|
+
* track() is a no-op, telemetry is forced off, and the client starts
|
|
158
|
+
* "initialized" with an empty blob. Prefer the {@link FlagsClient.forTesting}
|
|
159
|
+
* factory over passing this directly.
|
|
160
|
+
*/
|
|
161
|
+
testMode?: boolean;
|
|
162
|
+
}
|
|
163
|
+
declare class FlagsClient {
|
|
164
|
+
private readonly apiKey;
|
|
165
|
+
private readonly baseUrl;
|
|
166
|
+
private readonly env;
|
|
167
|
+
private readonly privateAttributes;
|
|
168
|
+
private readonly stickyStore;
|
|
169
|
+
private readonly telemetry;
|
|
170
|
+
private readonly seeLimiter;
|
|
171
|
+
private flagsBlob;
|
|
172
|
+
private expsBlob;
|
|
173
|
+
private flagsEtag;
|
|
174
|
+
private expsEtag;
|
|
175
|
+
private pollInterval;
|
|
176
|
+
private timer;
|
|
177
|
+
private initialized;
|
|
178
|
+
private readonly testMode;
|
|
179
|
+
private readonly flagOverrides;
|
|
180
|
+
private readonly configOverrides;
|
|
181
|
+
private readonly experimentOverrides;
|
|
182
|
+
private readonly changeListeners;
|
|
183
|
+
constructor(opts: FlagsClientOptions);
|
|
184
|
+
/**
|
|
185
|
+
* Build a no-network, immediately-usable client for tests (Statsig-style).
|
|
186
|
+
* init()/initOnce() are no-ops (never fetch), track() is a no-op, telemetry
|
|
187
|
+
* is disabled, and the client is already "initialized" — seed every entity
|
|
188
|
+
* with overrideFlag/overrideConfig/overrideExperiment. No SDK key required.
|
|
189
|
+
*
|
|
190
|
+
* ```ts
|
|
191
|
+
* const client = FlagsClient.forTesting();
|
|
192
|
+
* client.overrideFlag("new_checkout", true);
|
|
193
|
+
* client.getFlag("new_checkout", { user_id: "u1" }); // true
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
static forTesting(opts?: Partial<FlagsClientOptions>): FlagsClient;
|
|
197
|
+
/**
|
|
198
|
+
* Build a fully OFFLINE client from a pre-captured snapshot — no network ever.
|
|
199
|
+
* Reuses the test-mode plumbing (init()/initOnce()/track() are no-ops,
|
|
200
|
+
* telemetry off) but, unlike forTesting(), seeds the REAL flags + experiments
|
|
201
|
+
* blobs so evaluations run the canonical eval against the snapshot. Local
|
|
202
|
+
* overrides still apply on top.
|
|
203
|
+
*
|
|
204
|
+
* Snapshot shape mirrors the wire bodies:
|
|
205
|
+
* `{ flags: <GET /sdk/flags body>, experiments: <GET /sdk/experiments body> }`.
|
|
206
|
+
*/
|
|
207
|
+
static fromSnapshot(snapshot: {
|
|
208
|
+
flags: FlagsBlob;
|
|
209
|
+
experiments: ExpsBlob;
|
|
210
|
+
}): FlagsClient;
|
|
211
|
+
/**
|
|
212
|
+
* Build a fully OFFLINE client from a snapshot JSON file on disk (Node only —
|
|
213
|
+
* not available in the browser entrypoint). The file must contain
|
|
214
|
+
* `{ "flags": <GET /sdk/flags body>, "experiments": <GET /sdk/experiments body> }`.
|
|
215
|
+
* See {@link FlagsClient.fromSnapshot}.
|
|
216
|
+
*/
|
|
217
|
+
static fromFile(path: string): FlagsClient;
|
|
218
|
+
init(): Promise<void>;
|
|
219
|
+
initOnce(): Promise<void>;
|
|
220
|
+
/** Force `getFlag(name, …)` to return `value`, ignoring the fetched gate. */
|
|
221
|
+
overrideFlag(name: string, value: boolean): void;
|
|
222
|
+
/** Force `getConfig(name)` to return `value`, ignoring the fetched config. */
|
|
223
|
+
overrideConfig(name: string, value: unknown): void;
|
|
224
|
+
/**
|
|
225
|
+
* Force `getExperiment(name, …)` to return `{ inExperiment: true, group, params }`,
|
|
226
|
+
* ignoring allocation, holdouts, and targeting.
|
|
227
|
+
*/
|
|
228
|
+
overrideExperiment(name: string, group: string, params: Record<string, unknown>): void;
|
|
229
|
+
/** Remove every programmatic override set via the override* methods. */
|
|
230
|
+
clearOverrides(): void;
|
|
231
|
+
destroy(): void;
|
|
232
|
+
/**
|
|
233
|
+
* Subscribe to data-change notifications. The listener fires after a
|
|
234
|
+
* background poll fetch returns NEW data (200, not 304) — i.e. after the
|
|
235
|
+
* cached blob is updated. Never fires in testMode/offline (no polling).
|
|
236
|
+
* Returns an unsubscribe function.
|
|
237
|
+
*/
|
|
238
|
+
onChange(listener: () => void): () => void;
|
|
239
|
+
private notifyChange;
|
|
240
|
+
private startPoll;
|
|
241
|
+
private fetchAll;
|
|
242
|
+
private fetchFlags;
|
|
243
|
+
private fetchExps;
|
|
244
|
+
/**
|
|
245
|
+
* Evaluate a gate and report WHY (LaunchDarkly variationDetail parity). The
|
|
246
|
+
* reason is computed entirely at this boundary — the canonical eval
|
|
247
|
+
* (evalGateInternal) is untouched. A local override short-circuits BEFORE
|
|
248
|
+
* telemetry, exactly like getFlag's override path; otherwise exactly one
|
|
249
|
+
* "gate" beacon is emitted.
|
|
250
|
+
*/
|
|
251
|
+
getFlagDetail(name: string, user: User): FlagDetail;
|
|
252
|
+
/**
|
|
253
|
+
* Read a feature gate. Returns `defaultValue` ONLY when the gate cannot be
|
|
254
|
+
* evaluated (client not initialized or flag not found) — never for a gate that
|
|
255
|
+
* legitimately evaluates to false. Plain `getFlag(name, user)` keeps returning
|
|
256
|
+
* false for a missing flag.
|
|
257
|
+
*/
|
|
258
|
+
getFlag(name: string, user: User, defaultValue?: boolean): boolean;
|
|
259
|
+
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
260
|
+
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
261
|
+
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
262
|
+
/** Drop caller-marked private attributes from an outbound props bag. */
|
|
263
|
+
private stripPrivate;
|
|
264
|
+
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
265
|
+
/**
|
|
266
|
+
* Emit an exposure event for an experiment at the server-side decision point
|
|
267
|
+
* (parity with the browser's auto-exposure). The server is stateless and
|
|
268
|
+
* never auto-logs, so call this when you actually present the treatment.
|
|
269
|
+
* Re-evaluates the experiment for `user` (a bare `user_id` string is wrapped
|
|
270
|
+
* as `{ user_id }`); if the user is enrolled, POSTs a single exposure to
|
|
271
|
+
* `/collect`. No-op in test mode or when the user isn't enrolled.
|
|
272
|
+
*/
|
|
273
|
+
logExposure(user: string | User, name: string): void;
|
|
274
|
+
/**
|
|
275
|
+
* Report a structured error into the errors primitive. Fire-and-forget —
|
|
276
|
+
* never blocks or throws into the request path. Spam-guarded by a 30s
|
|
277
|
+
* dedup window + per-process cap.
|
|
278
|
+
*/
|
|
279
|
+
reportError(problem: unknown, consequence: Consequence, extras?: SeeExtras, kind?: SeeKind): void;
|
|
280
|
+
/**
|
|
281
|
+
* Evaluate all flags, configs, and experiments for a user against the locally
|
|
282
|
+
* cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
|
|
283
|
+
* overrides from the request URL when provided.
|
|
284
|
+
*
|
|
285
|
+
* Intended for SSR: call on the server, inject the result as
|
|
286
|
+
* `window.__SE_BOOTSTRAP` in the HTML, and the client SDK will read it
|
|
287
|
+
* synchronously without waiting for identify() to resolve.
|
|
288
|
+
*/
|
|
289
|
+
evaluate(user: User, rawUrl?: string): BootstrapPayload;
|
|
290
|
+
getKillswitch(name: string, switchKey?: string): boolean;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* OpenFeature **server** provider for Shipeasy.
|
|
295
|
+
*
|
|
296
|
+
* Lets apps standardized on the CNCF OpenFeature API plug Shipeasy in as the
|
|
297
|
+
* backing provider:
|
|
298
|
+
*
|
|
299
|
+
* ```ts
|
|
300
|
+
* import { OpenFeature } from "@openfeature/server-sdk";
|
|
301
|
+
* import { FlagsClient } from "@shipeasy/sdk/server";
|
|
302
|
+
* import { ShipeasyProvider } from "@shipeasy/sdk/openfeature-server";
|
|
303
|
+
*
|
|
304
|
+
* const client = new FlagsClient({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
|
|
305
|
+
* await OpenFeature.setProviderAndWait(new ShipeasyProvider(client));
|
|
306
|
+
*
|
|
307
|
+
* const ofClient = OpenFeature.getClient();
|
|
308
|
+
* const on = await ofClient.getBooleanValue("new_checkout", false, { targetingKey: "u1" });
|
|
309
|
+
* ```
|
|
310
|
+
*
|
|
311
|
+
* Pure adapter over `FlagsClient` — no change to evaluation. `@openfeature/server-sdk`
|
|
312
|
+
* is an optional peer dependency; install it in the consuming app.
|
|
313
|
+
*/
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Shipeasy OpenFeature provider (server paradigm). Wraps a `FlagsClient`;
|
|
317
|
+
* evaluation is local against the cached blob, so resolution is effectively
|
|
318
|
+
* synchronous (the methods are `async` only to satisfy the server contract).
|
|
319
|
+
*/
|
|
320
|
+
declare class ShipeasyProvider implements Provider {
|
|
321
|
+
private readonly client;
|
|
322
|
+
readonly metadata: {
|
|
323
|
+
readonly name: "shipeasy";
|
|
324
|
+
};
|
|
325
|
+
readonly runsOn: "server";
|
|
326
|
+
constructor(client: FlagsClient);
|
|
327
|
+
/** Fetch the rules blob once. The SDK fires `Ready` when this resolves. */
|
|
328
|
+
initialize(): Promise<void>;
|
|
329
|
+
onClose(): Promise<void>;
|
|
330
|
+
resolveBooleanEvaluation(flagKey: string, defaultValue: boolean, context: EvaluationContext, _logger?: Logger): Promise<ResolutionDetails<boolean>>;
|
|
331
|
+
resolveStringEvaluation(flagKey: string, defaultValue: string, _context: EvaluationContext, _logger?: Logger): Promise<ResolutionDetails<string>>;
|
|
332
|
+
resolveNumberEvaluation(flagKey: string, defaultValue: number, _context: EvaluationContext, _logger?: Logger): Promise<ResolutionDetails<number>>;
|
|
333
|
+
resolveObjectEvaluation<T extends JsonValue>(flagKey: string, defaultValue: T, _context: EvaluationContext, _logger?: Logger): Promise<ResolutionDetails<T>>;
|
|
334
|
+
/** OpenFeature `track()` → Shipeasy `track()`. No-ops without a targeting key. */
|
|
335
|
+
track(trackingEventName: string, context: EvaluationContext, details?: Record<string, unknown>): void;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export { ShipeasyProvider };
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { Provider, EvaluationContext, Logger, ResolutionDetails, JsonValue } from '@openfeature/server-sdk';
|
|
2
|
+
|
|
3
|
+
type SeeExtras = Record<string, string | number | boolean | null | undefined>;
|
|
4
|
+
type SeeKind = "caught" | "uncaught" | "unhandled_rejection" | "network" | "violation";
|
|
5
|
+
/** Built by `causesThe(subject).to(outcome)` — never constructed by hand. */
|
|
6
|
+
interface Consequence {
|
|
7
|
+
readonly __seConsequence: true;
|
|
8
|
+
readonly subject: string;
|
|
9
|
+
readonly outcome: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface User {
|
|
13
|
+
user_id?: string;
|
|
14
|
+
anonymous_id?: string;
|
|
15
|
+
[attr: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
interface ExperimentResult<P> {
|
|
18
|
+
inExperiment: boolean;
|
|
19
|
+
group: string;
|
|
20
|
+
params: P;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Why a flag evaluated the way it did (LaunchDarkly variationDetail parity).
|
|
24
|
+
* Computed at the client boundary, never inside the canonical eval:
|
|
25
|
+
* - CLIENT_NOT_READY — no rules blob loaded yet (init()/initOnce() pending)
|
|
26
|
+
* - FLAG_NOT_FOUND — the gate name isn't present in the loaded blob
|
|
27
|
+
* - OFF — the gate exists but is disabled / killed
|
|
28
|
+
* - OVERRIDE — a local override (overrideFlag) decided the value
|
|
29
|
+
* - RULE_MATCH — the gate evaluated true (rules + rollout passed)
|
|
30
|
+
* - DEFAULT — the gate evaluated false (a rule or the rollout denied)
|
|
31
|
+
*/
|
|
32
|
+
declare const FLAG_REASONS: readonly ["CLIENT_NOT_READY", "FLAG_NOT_FOUND", "OFF", "OVERRIDE", "RULE_MATCH", "DEFAULT"];
|
|
33
|
+
type FlagReason = (typeof FLAG_REASONS)[number];
|
|
34
|
+
interface FlagDetail {
|
|
35
|
+
value: boolean;
|
|
36
|
+
reason: FlagReason;
|
|
37
|
+
}
|
|
38
|
+
/** Options object form of `getConfig` — keeps the legacy `decode` callback and
|
|
39
|
+
* adds a `defaultValue` returned when the config key is absent. */
|
|
40
|
+
interface GetConfigOptions<T = unknown> {
|
|
41
|
+
/** Decode the raw stored value into the typed shape callers want. */
|
|
42
|
+
decode?: (raw: unknown) => T;
|
|
43
|
+
/** Returned when the config key is absent (not overridden, not in the blob). */
|
|
44
|
+
defaultValue?: T;
|
|
45
|
+
}
|
|
46
|
+
interface GateRule {
|
|
47
|
+
attr: string;
|
|
48
|
+
op: "eq" | "neq" | "in" | "not_in" | "gt" | "gte" | "lt" | "lte" | "contains" | "regex";
|
|
49
|
+
value: unknown;
|
|
50
|
+
}
|
|
51
|
+
interface Gate {
|
|
52
|
+
rules: GateRule[];
|
|
53
|
+
rolloutPct: number;
|
|
54
|
+
salt: string;
|
|
55
|
+
enabled: 0 | 1 | boolean;
|
|
56
|
+
killswitch?: 0 | 1 | boolean;
|
|
57
|
+
}
|
|
58
|
+
interface ExperimentGroup {
|
|
59
|
+
name: string;
|
|
60
|
+
weight: number;
|
|
61
|
+
params: Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
interface Experiment {
|
|
64
|
+
universe: string;
|
|
65
|
+
targetingGate?: string | null;
|
|
66
|
+
allocationPct: number;
|
|
67
|
+
salt: string;
|
|
68
|
+
groups: ExperimentGroup[];
|
|
69
|
+
status: "draft" | "running" | "stopped" | "archived";
|
|
70
|
+
/** Attribute to bucket on (e.g. company_id); defaults to user_id/anonymous_id. */
|
|
71
|
+
bucketBy?: string | null;
|
|
72
|
+
}
|
|
73
|
+
/** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
|
|
74
|
+
interface StickyEntry {
|
|
75
|
+
g: string;
|
|
76
|
+
s: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Pluggable sticky-bucketing store for the server (doc 20 §2). Keyed by the
|
|
80
|
+
* bucketing unit; the value is that unit's per-experiment assignments. Absent
|
|
81
|
+
* from {@link FlagsClientOptions} ⇒ today's deterministic behaviour. Use
|
|
82
|
+
* {@link createInMemoryStickyStore} or a cookie-bridge built from request
|
|
83
|
+
* cookies.
|
|
84
|
+
*/
|
|
85
|
+
interface StickyBucketStore {
|
|
86
|
+
get(unit: string): Record<string, StickyEntry> | undefined;
|
|
87
|
+
set(unit: string, exp: string, entry: StickyEntry): void;
|
|
88
|
+
}
|
|
89
|
+
interface Universe {
|
|
90
|
+
holdout_range: [number, number] | null;
|
|
91
|
+
}
|
|
92
|
+
interface Killswitch {
|
|
93
|
+
killed: 0 | 1 | boolean;
|
|
94
|
+
switches?: Record<string, 0 | 1 | boolean>;
|
|
95
|
+
}
|
|
96
|
+
/** Body of `GET /sdk/flags` — the snapshot's `flags` field. See {@link FlagsClient.fromSnapshot}. */
|
|
97
|
+
interface FlagsBlob {
|
|
98
|
+
version: string;
|
|
99
|
+
plan: string;
|
|
100
|
+
gates: Record<string, Gate>;
|
|
101
|
+
configs: Record<string, {
|
|
102
|
+
value: unknown;
|
|
103
|
+
}>;
|
|
104
|
+
killswitches: Record<string, Killswitch>;
|
|
105
|
+
}
|
|
106
|
+
/** Body of `GET /sdk/experiments` — the snapshot's `experiments` field. See {@link FlagsClient.fromSnapshot}. */
|
|
107
|
+
interface ExpsBlob {
|
|
108
|
+
version: string;
|
|
109
|
+
universes: Record<string, Universe>;
|
|
110
|
+
experiments: Record<string, Experiment>;
|
|
111
|
+
}
|
|
112
|
+
interface BootstrapPayload {
|
|
113
|
+
flags: Record<string, boolean>;
|
|
114
|
+
configs: Record<string, unknown>;
|
|
115
|
+
experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
|
|
116
|
+
killswitches: Record<string, boolean | Record<string, boolean>>;
|
|
117
|
+
}
|
|
118
|
+
type FlagsClientEnv = "dev" | "staging" | "prod";
|
|
119
|
+
interface FlagsClientOptions {
|
|
120
|
+
apiKey: string;
|
|
121
|
+
baseUrl?: string;
|
|
122
|
+
/** Which published env to read values from. Defaults to "prod". */
|
|
123
|
+
env?: FlagsClientEnv;
|
|
124
|
+
/**
|
|
125
|
+
* Preload the flags blob synchronously without a network fetch. Primarily
|
|
126
|
+
* for tests; production callers should rely on init()/initOnce().
|
|
127
|
+
*/
|
|
128
|
+
initialBlob?: FlagsBlob;
|
|
129
|
+
/**
|
|
130
|
+
* Per-evaluation usage telemetry. ON by default — each getFlag/getConfig/
|
|
131
|
+
* getExperiment/getKillswitch (and the per-key evaluate() loop) fires one
|
|
132
|
+
* fire-and-forget beacon counted by Cloudflare's native per-path analytics.
|
|
133
|
+
* Pass `true` to disable. NOTE: on Cloudflare Workers each beacon is an
|
|
134
|
+
* outbound subrequest (cap 50 free / 1000 paid per invocation), so disable
|
|
135
|
+
* this on hot request paths that evaluate many flags per request.
|
|
136
|
+
*/
|
|
137
|
+
disableTelemetry?: boolean;
|
|
138
|
+
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
139
|
+
telemetryUrl?: string;
|
|
140
|
+
/**
|
|
141
|
+
* Attribute names usable for targeting but never persisted in analytics
|
|
142
|
+
* (LD/Statsig `privateAttributes`). The server evaluates locally so private
|
|
143
|
+
* attrs never leave for evaluation at all; the only egress is `/collect`, and
|
|
144
|
+
* the listed keys are stripped from every outbound `track()` payload.
|
|
145
|
+
*/
|
|
146
|
+
privateAttributes?: string[];
|
|
147
|
+
/**
|
|
148
|
+
* Sticky-bucketing store (doc 20 §2). When provided, `getExperiment` locks a
|
|
149
|
+
* unit to its first-assigned variant — changing allocation % or weights won't
|
|
150
|
+
* re-bucket enrolled units (changing the experiment salt is the reshuffle
|
|
151
|
+
* lever). Absent ⇒ deterministic (fully backward compatible). Built-ins:
|
|
152
|
+
* {@link createInMemoryStickyStore}, or a cookie-bridge over `__se_sticky`.
|
|
153
|
+
*/
|
|
154
|
+
stickyStore?: StickyBucketStore;
|
|
155
|
+
/**
|
|
156
|
+
* Test mode — no network at all. init()/initOnce() are no-ops (never fetch),
|
|
157
|
+
* track() is a no-op, telemetry is forced off, and the client starts
|
|
158
|
+
* "initialized" with an empty blob. Prefer the {@link FlagsClient.forTesting}
|
|
159
|
+
* factory over passing this directly.
|
|
160
|
+
*/
|
|
161
|
+
testMode?: boolean;
|
|
162
|
+
}
|
|
163
|
+
declare class FlagsClient {
|
|
164
|
+
private readonly apiKey;
|
|
165
|
+
private readonly baseUrl;
|
|
166
|
+
private readonly env;
|
|
167
|
+
private readonly privateAttributes;
|
|
168
|
+
private readonly stickyStore;
|
|
169
|
+
private readonly telemetry;
|
|
170
|
+
private readonly seeLimiter;
|
|
171
|
+
private flagsBlob;
|
|
172
|
+
private expsBlob;
|
|
173
|
+
private flagsEtag;
|
|
174
|
+
private expsEtag;
|
|
175
|
+
private pollInterval;
|
|
176
|
+
private timer;
|
|
177
|
+
private initialized;
|
|
178
|
+
private readonly testMode;
|
|
179
|
+
private readonly flagOverrides;
|
|
180
|
+
private readonly configOverrides;
|
|
181
|
+
private readonly experimentOverrides;
|
|
182
|
+
private readonly changeListeners;
|
|
183
|
+
constructor(opts: FlagsClientOptions);
|
|
184
|
+
/**
|
|
185
|
+
* Build a no-network, immediately-usable client for tests (Statsig-style).
|
|
186
|
+
* init()/initOnce() are no-ops (never fetch), track() is a no-op, telemetry
|
|
187
|
+
* is disabled, and the client is already "initialized" — seed every entity
|
|
188
|
+
* with overrideFlag/overrideConfig/overrideExperiment. No SDK key required.
|
|
189
|
+
*
|
|
190
|
+
* ```ts
|
|
191
|
+
* const client = FlagsClient.forTesting();
|
|
192
|
+
* client.overrideFlag("new_checkout", true);
|
|
193
|
+
* client.getFlag("new_checkout", { user_id: "u1" }); // true
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
static forTesting(opts?: Partial<FlagsClientOptions>): FlagsClient;
|
|
197
|
+
/**
|
|
198
|
+
* Build a fully OFFLINE client from a pre-captured snapshot — no network ever.
|
|
199
|
+
* Reuses the test-mode plumbing (init()/initOnce()/track() are no-ops,
|
|
200
|
+
* telemetry off) but, unlike forTesting(), seeds the REAL flags + experiments
|
|
201
|
+
* blobs so evaluations run the canonical eval against the snapshot. Local
|
|
202
|
+
* overrides still apply on top.
|
|
203
|
+
*
|
|
204
|
+
* Snapshot shape mirrors the wire bodies:
|
|
205
|
+
* `{ flags: <GET /sdk/flags body>, experiments: <GET /sdk/experiments body> }`.
|
|
206
|
+
*/
|
|
207
|
+
static fromSnapshot(snapshot: {
|
|
208
|
+
flags: FlagsBlob;
|
|
209
|
+
experiments: ExpsBlob;
|
|
210
|
+
}): FlagsClient;
|
|
211
|
+
/**
|
|
212
|
+
* Build a fully OFFLINE client from a snapshot JSON file on disk (Node only —
|
|
213
|
+
* not available in the browser entrypoint). The file must contain
|
|
214
|
+
* `{ "flags": <GET /sdk/flags body>, "experiments": <GET /sdk/experiments body> }`.
|
|
215
|
+
* See {@link FlagsClient.fromSnapshot}.
|
|
216
|
+
*/
|
|
217
|
+
static fromFile(path: string): FlagsClient;
|
|
218
|
+
init(): Promise<void>;
|
|
219
|
+
initOnce(): Promise<void>;
|
|
220
|
+
/** Force `getFlag(name, …)` to return `value`, ignoring the fetched gate. */
|
|
221
|
+
overrideFlag(name: string, value: boolean): void;
|
|
222
|
+
/** Force `getConfig(name)` to return `value`, ignoring the fetched config. */
|
|
223
|
+
overrideConfig(name: string, value: unknown): void;
|
|
224
|
+
/**
|
|
225
|
+
* Force `getExperiment(name, …)` to return `{ inExperiment: true, group, params }`,
|
|
226
|
+
* ignoring allocation, holdouts, and targeting.
|
|
227
|
+
*/
|
|
228
|
+
overrideExperiment(name: string, group: string, params: Record<string, unknown>): void;
|
|
229
|
+
/** Remove every programmatic override set via the override* methods. */
|
|
230
|
+
clearOverrides(): void;
|
|
231
|
+
destroy(): void;
|
|
232
|
+
/**
|
|
233
|
+
* Subscribe to data-change notifications. The listener fires after a
|
|
234
|
+
* background poll fetch returns NEW data (200, not 304) — i.e. after the
|
|
235
|
+
* cached blob is updated. Never fires in testMode/offline (no polling).
|
|
236
|
+
* Returns an unsubscribe function.
|
|
237
|
+
*/
|
|
238
|
+
onChange(listener: () => void): () => void;
|
|
239
|
+
private notifyChange;
|
|
240
|
+
private startPoll;
|
|
241
|
+
private fetchAll;
|
|
242
|
+
private fetchFlags;
|
|
243
|
+
private fetchExps;
|
|
244
|
+
/**
|
|
245
|
+
* Evaluate a gate and report WHY (LaunchDarkly variationDetail parity). The
|
|
246
|
+
* reason is computed entirely at this boundary — the canonical eval
|
|
247
|
+
* (evalGateInternal) is untouched. A local override short-circuits BEFORE
|
|
248
|
+
* telemetry, exactly like getFlag's override path; otherwise exactly one
|
|
249
|
+
* "gate" beacon is emitted.
|
|
250
|
+
*/
|
|
251
|
+
getFlagDetail(name: string, user: User): FlagDetail;
|
|
252
|
+
/**
|
|
253
|
+
* Read a feature gate. Returns `defaultValue` ONLY when the gate cannot be
|
|
254
|
+
* evaluated (client not initialized or flag not found) — never for a gate that
|
|
255
|
+
* legitimately evaluates to false. Plain `getFlag(name, user)` keeps returning
|
|
256
|
+
* false for a missing flag.
|
|
257
|
+
*/
|
|
258
|
+
getFlag(name: string, user: User, defaultValue?: boolean): boolean;
|
|
259
|
+
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
260
|
+
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
261
|
+
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
262
|
+
/** Drop caller-marked private attributes from an outbound props bag. */
|
|
263
|
+
private stripPrivate;
|
|
264
|
+
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
265
|
+
/**
|
|
266
|
+
* Emit an exposure event for an experiment at the server-side decision point
|
|
267
|
+
* (parity with the browser's auto-exposure). The server is stateless and
|
|
268
|
+
* never auto-logs, so call this when you actually present the treatment.
|
|
269
|
+
* Re-evaluates the experiment for `user` (a bare `user_id` string is wrapped
|
|
270
|
+
* as `{ user_id }`); if the user is enrolled, POSTs a single exposure to
|
|
271
|
+
* `/collect`. No-op in test mode or when the user isn't enrolled.
|
|
272
|
+
*/
|
|
273
|
+
logExposure(user: string | User, name: string): void;
|
|
274
|
+
/**
|
|
275
|
+
* Report a structured error into the errors primitive. Fire-and-forget —
|
|
276
|
+
* never blocks or throws into the request path. Spam-guarded by a 30s
|
|
277
|
+
* dedup window + per-process cap.
|
|
278
|
+
*/
|
|
279
|
+
reportError(problem: unknown, consequence: Consequence, extras?: SeeExtras, kind?: SeeKind): void;
|
|
280
|
+
/**
|
|
281
|
+
* Evaluate all flags, configs, and experiments for a user against the locally
|
|
282
|
+
* cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
|
|
283
|
+
* overrides from the request URL when provided.
|
|
284
|
+
*
|
|
285
|
+
* Intended for SSR: call on the server, inject the result as
|
|
286
|
+
* `window.__SE_BOOTSTRAP` in the HTML, and the client SDK will read it
|
|
287
|
+
* synchronously without waiting for identify() to resolve.
|
|
288
|
+
*/
|
|
289
|
+
evaluate(user: User, rawUrl?: string): BootstrapPayload;
|
|
290
|
+
getKillswitch(name: string, switchKey?: string): boolean;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* OpenFeature **server** provider for Shipeasy.
|
|
295
|
+
*
|
|
296
|
+
* Lets apps standardized on the CNCF OpenFeature API plug Shipeasy in as the
|
|
297
|
+
* backing provider:
|
|
298
|
+
*
|
|
299
|
+
* ```ts
|
|
300
|
+
* import { OpenFeature } from "@openfeature/server-sdk";
|
|
301
|
+
* import { FlagsClient } from "@shipeasy/sdk/server";
|
|
302
|
+
* import { ShipeasyProvider } from "@shipeasy/sdk/openfeature-server";
|
|
303
|
+
*
|
|
304
|
+
* const client = new FlagsClient({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
|
|
305
|
+
* await OpenFeature.setProviderAndWait(new ShipeasyProvider(client));
|
|
306
|
+
*
|
|
307
|
+
* const ofClient = OpenFeature.getClient();
|
|
308
|
+
* const on = await ofClient.getBooleanValue("new_checkout", false, { targetingKey: "u1" });
|
|
309
|
+
* ```
|
|
310
|
+
*
|
|
311
|
+
* Pure adapter over `FlagsClient` — no change to evaluation. `@openfeature/server-sdk`
|
|
312
|
+
* is an optional peer dependency; install it in the consuming app.
|
|
313
|
+
*/
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Shipeasy OpenFeature provider (server paradigm). Wraps a `FlagsClient`;
|
|
317
|
+
* evaluation is local against the cached blob, so resolution is effectively
|
|
318
|
+
* synchronous (the methods are `async` only to satisfy the server contract).
|
|
319
|
+
*/
|
|
320
|
+
declare class ShipeasyProvider implements Provider {
|
|
321
|
+
private readonly client;
|
|
322
|
+
readonly metadata: {
|
|
323
|
+
readonly name: "shipeasy";
|
|
324
|
+
};
|
|
325
|
+
readonly runsOn: "server";
|
|
326
|
+
constructor(client: FlagsClient);
|
|
327
|
+
/** Fetch the rules blob once. The SDK fires `Ready` when this resolves. */
|
|
328
|
+
initialize(): Promise<void>;
|
|
329
|
+
onClose(): Promise<void>;
|
|
330
|
+
resolveBooleanEvaluation(flagKey: string, defaultValue: boolean, context: EvaluationContext, _logger?: Logger): Promise<ResolutionDetails<boolean>>;
|
|
331
|
+
resolveStringEvaluation(flagKey: string, defaultValue: string, _context: EvaluationContext, _logger?: Logger): Promise<ResolutionDetails<string>>;
|
|
332
|
+
resolveNumberEvaluation(flagKey: string, defaultValue: number, _context: EvaluationContext, _logger?: Logger): Promise<ResolutionDetails<number>>;
|
|
333
|
+
resolveObjectEvaluation<T extends JsonValue>(flagKey: string, defaultValue: T, _context: EvaluationContext, _logger?: Logger): Promise<ResolutionDetails<T>>;
|
|
334
|
+
/** OpenFeature `track()` → Shipeasy `track()`. No-ops without a targeting key. */
|
|
335
|
+
track(trackingEventName: string, context: EvaluationContext, details?: Record<string, unknown>): void;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export { ShipeasyProvider };
|