@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/README.md
CHANGED
|
@@ -130,6 +130,164 @@ The loader IIFE is published to a public R2 bucket on every release and
|
|
|
130
130
|
cached for 1y at `loader-vX.Y.Z.js` (immutable) plus a rolling 5-minute
|
|
131
131
|
`loader.js`.
|
|
132
132
|
|
|
133
|
+
## Testing
|
|
134
|
+
|
|
135
|
+
For unit tests, build a **no-network** client with `forTesting()` and seed every
|
|
136
|
+
entity with local overrides (Statsig-style). In test mode the client is already
|
|
137
|
+
"initialized"/"ready", `init()`/`initOnce()`/`identify()` are no-ops (they never
|
|
138
|
+
fetch), `track()` is a no-op, telemetry is disabled, and no SDK key is required —
|
|
139
|
+
so your tests never touch the network.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
// Server (Node / Cloudflare Worker / Deno)
|
|
143
|
+
import { FlagsClient } from "@shipeasy/sdk/server";
|
|
144
|
+
|
|
145
|
+
const client = FlagsClient.forTesting();
|
|
146
|
+
|
|
147
|
+
client.overrideFlag("new_checkout", true);
|
|
148
|
+
client.overrideConfig("upload_limits", { max_uploads: 50 });
|
|
149
|
+
client.overrideExperiment("hero_cta", "treatment", { primary_label: "Buy now" });
|
|
150
|
+
|
|
151
|
+
client.getFlag("new_checkout", { user_id: "u1" }); // true
|
|
152
|
+
client.getConfig("upload_limits"); // { max_uploads: 50 }
|
|
153
|
+
client.getExperiment("hero_cta", { user_id: "u1" }, { primary_label: "Sign up" });
|
|
154
|
+
// → { inExperiment: true, group: "treatment", params: { primary_label: "Buy now" } }
|
|
155
|
+
|
|
156
|
+
client.track("u1", "purchase"); // no-op — never hits the network
|
|
157
|
+
client.clearOverrides(); // reset every override back to default
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
// Browser (vanilla JS — no React required)
|
|
162
|
+
import { FlagsClientBrowser } from "@shipeasy/sdk/client";
|
|
163
|
+
|
|
164
|
+
const client = FlagsClientBrowser.forTesting();
|
|
165
|
+
|
|
166
|
+
client.overrideFlag("new_checkout", true);
|
|
167
|
+
client.overrideConfig("upload_limits", { max_uploads: 50 });
|
|
168
|
+
client.overrideExperiment("hero_cta", "treatment", { primary_label: "Buy now" });
|
|
169
|
+
|
|
170
|
+
client.getFlag("new_checkout"); // true
|
|
171
|
+
client.getConfig("upload_limits"); // { max_uploads: 50 }
|
|
172
|
+
client.getExperiment("hero_cta", { primary_label: "Sign up" });
|
|
173
|
+
// → { inExperiment: true, group: "treatment", params: { primary_label: "Buy now" } }
|
|
174
|
+
|
|
175
|
+
client.track("purchase"); // no-op
|
|
176
|
+
client.clearOverrides();
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The `override*` setters also work on a **normal** client (not just `forTesting()`),
|
|
180
|
+
mirroring Statsig — a programmatic override always wins over the fetched values.
|
|
181
|
+
In the browser the precedence is: programmatic override > URL/devtools override
|
|
182
|
+
(`?se_gate_…` / `?se_config_…` / `?se_exp_…`) > the server's evaluation.
|
|
183
|
+
|
|
184
|
+
**API:**
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
overrideFlag(name: string, value: boolean): void;
|
|
188
|
+
overrideConfig(name: string, value: unknown): void;
|
|
189
|
+
overrideExperiment(name: string, group: string, params: Record<string, unknown>): void;
|
|
190
|
+
clearOverrides(): void;
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Default values
|
|
194
|
+
|
|
195
|
+
`getFlag` / `getConfig` take a caller-supplied default that is returned **only
|
|
196
|
+
when the value can't be evaluated** — the client isn't initialized yet, or the
|
|
197
|
+
key isn't in the loaded rules. A flag that legitimately evaluates to `false`
|
|
198
|
+
(disabled, rule denied, rolled out to 0%) still returns `false`, never the
|
|
199
|
+
default.
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
// Server
|
|
203
|
+
flags.get("new_checkout", { user_id: "u1" }); // false for a missing flag
|
|
204
|
+
flags.get("new_checkout", { user_id: "u1" }, true); // true only if not-ready/not-found
|
|
205
|
+
flags.getConfig("limits", { defaultValue: { max: 50 } }); // default when key absent
|
|
206
|
+
|
|
207
|
+
// Browser (no user arg)
|
|
208
|
+
client.getFlag("new_checkout"); // false for a missing flag
|
|
209
|
+
client.getFlag("new_checkout", true); // default when not-ready/not-found
|
|
210
|
+
client.getConfig("limits", { defaultValue: { max: 50 } });
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The legacy `getConfig(name, decode)` callback signature still works unchanged;
|
|
214
|
+
the options object (`{ decode?, defaultValue? }`) is the new, additive form.
|
|
215
|
+
|
|
216
|
+
## Evaluation detail
|
|
217
|
+
|
|
218
|
+
`getFlagDetail(name[, user])` returns `{ value, reason }` (LaunchDarkly
|
|
219
|
+
`variationDetail` parity) so you can see *why* a flag resolved the way it did:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import type { FlagReason } from "@shipeasy/sdk/server"; // or /client
|
|
223
|
+
|
|
224
|
+
const d = client.getFlagDetail("new_checkout", { user_id: "u1" });
|
|
225
|
+
// → { value: true, reason: "RULE_MATCH" }
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`FlagReason` is one of:
|
|
229
|
+
|
|
230
|
+
| reason | meaning |
|
|
231
|
+
| ------------------ | ---------------------------------------------------------- |
|
|
232
|
+
| `CLIENT_NOT_READY` | no rules loaded yet (`init()` / `identify()` pending) |
|
|
233
|
+
| `FLAG_NOT_FOUND` | the gate name isn't in the loaded rules |
|
|
234
|
+
| `OFF` | the gate exists but is disabled / killed (server only) |
|
|
235
|
+
| `OVERRIDE` | a local override (or `?se_gate_…` URL override) decided it |
|
|
236
|
+
| `RULE_MATCH` | the gate evaluated `true` |
|
|
237
|
+
| `DEFAULT` | the gate evaluated `false` |
|
|
238
|
+
|
|
239
|
+
`getFlag` is implemented on top of `getFlagDetail` (single evaluation, single
|
|
240
|
+
telemetry beacon). On the browser the server pre-evaluates the gate's
|
|
241
|
+
enabled/killed state into a boolean, so `OFF` folds into `DEFAULT` there.
|
|
242
|
+
|
|
243
|
+
## Change listeners
|
|
244
|
+
|
|
245
|
+
The server `FlagsClient` fires registered listeners after a **background poll
|
|
246
|
+
returns new data** (HTTP 200, not 304) — handy for invalidating a cache or
|
|
247
|
+
re-rendering when a flag flips. Returns an unsubscribe function. Never fires in
|
|
248
|
+
test/offline mode (no polling happens):
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
const client = new FlagsClient({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
|
|
252
|
+
await client.init();
|
|
253
|
+
const unsubscribe = client.onChange(() => {
|
|
254
|
+
console.log("flag rules changed — re-evaluating");
|
|
255
|
+
});
|
|
256
|
+
// later…
|
|
257
|
+
unsubscribe();
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The browser `FlagsClientBrowser` already exposes the equivalent `subscribe()`
|
|
261
|
+
(fires after each `identify()` / override change).
|
|
262
|
+
|
|
263
|
+
## Offline snapshot
|
|
264
|
+
|
|
265
|
+
Build a fully offline server client from a captured snapshot — zero network.
|
|
266
|
+
Evaluations run the real eval against the snapshot; `init()`/`initOnce()`/
|
|
267
|
+
`track()` are no-ops and overrides still apply on top. The snapshot is just the
|
|
268
|
+
two SDK wire bodies:
|
|
269
|
+
|
|
270
|
+
```jsonc
|
|
271
|
+
// snapshot.json
|
|
272
|
+
{
|
|
273
|
+
"flags": /* body of GET /sdk/flags */,
|
|
274
|
+
"experiments": /* body of GET /sdk/experiments */
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
import { FlagsClient } from "@shipeasy/sdk/server";
|
|
280
|
+
|
|
281
|
+
const client = FlagsClient.fromFile("./snapshot.json");
|
|
282
|
+
// or, if you already hold the parsed object:
|
|
283
|
+
const client = FlagsClient.fromSnapshot({ flags, experiments });
|
|
284
|
+
|
|
285
|
+
client.getFlag("new_checkout", { user_id: "u1" });
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`fromFile` is Node-only (it reads the file with `node:fs`); `fromSnapshot` works
|
|
289
|
+
anywhere.
|
|
290
|
+
|
|
133
291
|
## Devtools overlay
|
|
134
292
|
|
|
135
293
|
Press `Shift+Alt+S` on any page running the SDK (or append `?se=1` to the
|
package/dist/client/index.d.mts
CHANGED
|
@@ -120,6 +120,47 @@ interface ExperimentResult<P> {
|
|
|
120
120
|
group: string;
|
|
121
121
|
params: P;
|
|
122
122
|
}
|
|
123
|
+
/** Options object form of `getExperiment` — the legacy `decode`/`variants`
|
|
124
|
+
* positional args plus per-call exposure control. */
|
|
125
|
+
interface GetExperimentOptions<P> {
|
|
126
|
+
/** Decode the raw stored params into the typed shape callers want. */
|
|
127
|
+
decode?: (raw: unknown) => P;
|
|
128
|
+
/** Variant-specific param overrides merged on top of group params. */
|
|
129
|
+
variants?: Record<string, Partial<P>>;
|
|
130
|
+
/**
|
|
131
|
+
* Override automatic exposure logging for this read. Defaults to the client's
|
|
132
|
+
* setting (`disableAutoExposure` flips it). `false` reads the variant without
|
|
133
|
+
* logging an exposure — pair with `logExposure(name)` at render time.
|
|
134
|
+
*/
|
|
135
|
+
logExposure?: boolean;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Why a flag evaluated the way it did (LaunchDarkly variationDetail parity).
|
|
139
|
+
* Computed at the client boundary:
|
|
140
|
+
* - CLIENT_NOT_READY — no eval result yet (identify() hasn't resolved)
|
|
141
|
+
* - FLAG_NOT_FOUND — the gate name isn't present in the eval result
|
|
142
|
+
* - OFF — folded into DEFAULT here (the server pre-evaluates the
|
|
143
|
+
* gate's enabled/killed state into a plain boolean, so the browser can't
|
|
144
|
+
* distinguish a disabled gate from one a rule denied)
|
|
145
|
+
* - OVERRIDE — a local override or a ?se_gate_/?se_ks_ URL override
|
|
146
|
+
* decided the value
|
|
147
|
+
* - RULE_MATCH — the gate evaluated true
|
|
148
|
+
* - DEFAULT — the gate evaluated false
|
|
149
|
+
*/
|
|
150
|
+
declare const FLAG_REASONS: readonly ["CLIENT_NOT_READY", "FLAG_NOT_FOUND", "OFF", "OVERRIDE", "RULE_MATCH", "DEFAULT"];
|
|
151
|
+
type FlagReason = (typeof FLAG_REASONS)[number];
|
|
152
|
+
interface FlagDetail {
|
|
153
|
+
value: boolean;
|
|
154
|
+
reason: FlagReason;
|
|
155
|
+
}
|
|
156
|
+
/** Options object form of `getConfig` — keeps the legacy `decode` callback and
|
|
157
|
+
* adds a `defaultValue` returned when the config key is absent. */
|
|
158
|
+
interface GetConfigOptions<T = unknown> {
|
|
159
|
+
/** Decode the raw stored value into the typed shape callers want. */
|
|
160
|
+
decode?: (raw: unknown) => T;
|
|
161
|
+
/** Returned when the config key is absent (not overridden, not in the eval result). */
|
|
162
|
+
defaultValue?: T;
|
|
163
|
+
}
|
|
123
164
|
interface EvalExpResult {
|
|
124
165
|
inExperiment: boolean;
|
|
125
166
|
group: string;
|
|
@@ -135,6 +176,17 @@ interface EvalResponse {
|
|
|
135
176
|
* switch booleans.
|
|
136
177
|
*/
|
|
137
178
|
killswitches?: Record<string, boolean | Record<string, boolean>>;
|
|
179
|
+
/**
|
|
180
|
+
* Newly-assigned sticky entries (doc 20 §2). The worker returns these so the
|
|
181
|
+
* browser can merge them into the `__se_sticky` cookie. Present only when
|
|
182
|
+
* sticky bucketing is on and at least one assignment was made/refreshed.
|
|
183
|
+
*/
|
|
184
|
+
sticky?: Record<string, StickyCookieEntry>;
|
|
185
|
+
}
|
|
186
|
+
/** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
|
|
187
|
+
interface StickyCookieEntry {
|
|
188
|
+
g: string;
|
|
189
|
+
s: string;
|
|
138
190
|
}
|
|
139
191
|
interface AutoCollectGroups {
|
|
140
192
|
vitals: boolean;
|
|
@@ -180,6 +232,36 @@ interface FlagsClientBrowserOptions {
|
|
|
180
232
|
disableTelemetry?: boolean;
|
|
181
233
|
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
182
234
|
telemetryUrl?: string;
|
|
235
|
+
/**
|
|
236
|
+
* Suppress automatic exposure logging in `getExperiment` (Statsig's
|
|
237
|
+
* `disableExposureLogging`). Default false — reading an enrolled variant
|
|
238
|
+
* auto-logs a deduped exposure. When true, no exposure fires unless you call
|
|
239
|
+
* `logExposure(name)` yourself, or pass `{ logExposure: true }` per call.
|
|
240
|
+
*/
|
|
241
|
+
disableAutoExposure?: boolean;
|
|
242
|
+
/**
|
|
243
|
+
* Attribute names usable for targeting but never persisted in analytics
|
|
244
|
+
* (LD/Statsig `privateAttributes`). They are sent to `/sdk/evaluate` under
|
|
245
|
+
* `private_attributes` so the edge can evaluate with them (unavoidable —
|
|
246
|
+
* the edge evaluates), but the worker never stores them, and the listed keys
|
|
247
|
+
* are stripped from any `track(props)` payload.
|
|
248
|
+
*/
|
|
249
|
+
privateAttributes?: string[];
|
|
250
|
+
/**
|
|
251
|
+
* Sticky bucketing (doc 20 §2). ON by default in the browser: a unit's
|
|
252
|
+
* first-assigned variant is locked in the `__se_sticky` cookie so changing an
|
|
253
|
+
* experiment's allocation % or group weights never silently re-buckets
|
|
254
|
+
* enrolled users. Changing the experiment salt is the deliberate reshuffle
|
|
255
|
+
* lever. Pass `false` to opt out (pure deterministic eval).
|
|
256
|
+
*/
|
|
257
|
+
stickyBucketing?: boolean;
|
|
258
|
+
/**
|
|
259
|
+
* Test mode — no network at all. identify()/init are no-ops (never call
|
|
260
|
+
* /sdk/evaluate), track() is a no-op, telemetry is forced off, and the client
|
|
261
|
+
* starts "ready" with an empty eval result. Prefer the
|
|
262
|
+
* {@link FlagsClientBrowser.forTesting} factory over passing this directly.
|
|
263
|
+
*/
|
|
264
|
+
testMode?: boolean;
|
|
183
265
|
}
|
|
184
266
|
declare class FlagsClientBrowser {
|
|
185
267
|
private readonly sdkKey;
|
|
@@ -187,6 +269,9 @@ declare class FlagsClientBrowser {
|
|
|
187
269
|
private readonly autoGuardrails;
|
|
188
270
|
private readonly autoGuardrailGroups;
|
|
189
271
|
private readonly autoCollectAlways;
|
|
272
|
+
private readonly disableAutoExposure;
|
|
273
|
+
private readonly privateAttributes;
|
|
274
|
+
private readonly stickyBucketing;
|
|
190
275
|
private readonly env;
|
|
191
276
|
private evalResult;
|
|
192
277
|
private anonId;
|
|
@@ -198,8 +283,26 @@ declare class FlagsClientBrowser {
|
|
|
198
283
|
private listeners;
|
|
199
284
|
private overrideListenerInstalled;
|
|
200
285
|
private identifySeq;
|
|
286
|
+
private readonly testMode;
|
|
287
|
+
private readonly flagOverrides;
|
|
288
|
+
private readonly configOverrides;
|
|
289
|
+
private readonly experimentOverrides;
|
|
201
290
|
private onOverrideChange;
|
|
202
291
|
constructor(opts: FlagsClientBrowserOptions);
|
|
292
|
+
/**
|
|
293
|
+
* Build a no-network, immediately-usable browser client for tests
|
|
294
|
+
* (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
|
|
295
|
+
* is a no-op, telemetry is disabled, and the client is already ready — seed
|
|
296
|
+
* every entity with overrideFlag/overrideConfig/overrideExperiment. No SDK
|
|
297
|
+
* key required.
|
|
298
|
+
*
|
|
299
|
+
* ```ts
|
|
300
|
+
* const client = FlagsClientBrowser.forTesting();
|
|
301
|
+
* client.overrideFlag("new_checkout", true);
|
|
302
|
+
* client.getFlag("new_checkout"); // true
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
static forTesting(opts?: Partial<FlagsClientBrowserOptions>): FlagsClientBrowser;
|
|
203
306
|
identify(user: User): Promise<void>;
|
|
204
307
|
/**
|
|
205
308
|
* Report a structured error into the errors primitive. Flushes immediately
|
|
@@ -210,9 +313,45 @@ declare class FlagsClientBrowser {
|
|
|
210
313
|
get ready(): boolean;
|
|
211
314
|
private notify;
|
|
212
315
|
initFromBootstrap(data: EvalResponse): void;
|
|
213
|
-
getFlag(name
|
|
316
|
+
/** Force `getFlag(name)` to return `value`, ignoring URL overrides + eval. */
|
|
317
|
+
overrideFlag(name: string, value: boolean): void;
|
|
318
|
+
/** Force `getConfig(name)` to return `value`, ignoring URL overrides + eval. */
|
|
319
|
+
overrideConfig(name: string, value: unknown): void;
|
|
320
|
+
/**
|
|
321
|
+
* Force `getExperiment(name, …)` to return `{ inExperiment: true, group, params }`,
|
|
322
|
+
* ignoring URL overrides, the eval result, and exposure logging.
|
|
323
|
+
*/
|
|
324
|
+
overrideExperiment(name: string, group: string, params: Record<string, unknown>): void;
|
|
325
|
+
/** Remove every programmatic override set via the override* methods. */
|
|
326
|
+
clearOverrides(): void;
|
|
327
|
+
/**
|
|
328
|
+
* Evaluate a gate and report WHY (LaunchDarkly variationDetail parity). A
|
|
329
|
+
* local override OR a ?se_gate_/?se_ks_ URL override short-circuits BEFORE
|
|
330
|
+
* telemetry; otherwise exactly one "gate" beacon is emitted. The server
|
|
331
|
+
* pre-evaluates a gate's enabled/killed state into a plain boolean, so OFF
|
|
332
|
+
* folds into DEFAULT here (the browser can't tell "disabled" from "rule
|
|
333
|
+
* denied").
|
|
334
|
+
*/
|
|
335
|
+
getFlagDetail(name: string): FlagDetail;
|
|
336
|
+
/**
|
|
337
|
+
* Read a feature gate. Returns `defaultValue` ONLY when the gate cannot be
|
|
338
|
+
* evaluated (not ready or flag not found) — never for a gate that legitimately
|
|
339
|
+
* evaluates to false. Plain `getFlag(name)` keeps returning false for a
|
|
340
|
+
* missing flag.
|
|
341
|
+
*/
|
|
342
|
+
getFlag(name: string, defaultValue?: boolean): boolean;
|
|
214
343
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
344
|
+
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
215
345
|
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
|
|
346
|
+
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, opts: GetExperimentOptions<P>): ExperimentResult<P>;
|
|
347
|
+
/**
|
|
348
|
+
* Manually log an exposure for an enrolled experiment (Statsig's
|
|
349
|
+
* `manuallyLogExposure`). Reads the cached eval result; if the visitor is in
|
|
350
|
+
* the experiment, pushes the session-deduped exposure. Pair this with the
|
|
351
|
+
* render of the treatment when reading with `{ logExposure: false }` (or
|
|
352
|
+
* `disableAutoExposure: true`). No-op if the visitor isn't enrolled.
|
|
353
|
+
*/
|
|
354
|
+
logExposure(name: string): void;
|
|
216
355
|
/**
|
|
217
356
|
* Subscribe to state changes — fires after identify() completes and on
|
|
218
357
|
* `se:override:change` events from the devtools overlay. Returns an
|
|
@@ -225,6 +364,8 @@ declare class FlagsClientBrowser {
|
|
|
225
364
|
*/
|
|
226
365
|
installBridge(): ShipeasySdkBridge | null;
|
|
227
366
|
track(eventName: string, props?: Record<string, unknown>): void;
|
|
367
|
+
/** Drop caller-marked private attributes from an outbound props bag. */
|
|
368
|
+
private stripPrivate;
|
|
228
369
|
/**
|
|
229
370
|
* Read a killswitch from the server's evaluated state. Without `switchKey`,
|
|
230
371
|
* returns true when the killswitch is whole-killed. With `switchKey`, returns
|
|
@@ -333,6 +474,25 @@ interface ShipeasyClientConfig {
|
|
|
333
474
|
* counted by Cloudflare's native per-path analytics. Pass `true` to opt out.
|
|
334
475
|
*/
|
|
335
476
|
disableTelemetry?: boolean;
|
|
477
|
+
/**
|
|
478
|
+
* Suppress automatic exposure logging in `flags.getExperiment` (Statsig's
|
|
479
|
+
* `disableExposureLogging`). Default false. When true, call
|
|
480
|
+
* `flags.logExposure(name)` at the treatment's render to log the exposure.
|
|
481
|
+
*/
|
|
482
|
+
disableAutoExposure?: boolean;
|
|
483
|
+
/**
|
|
484
|
+
* Attribute names usable for targeting but never persisted in analytics
|
|
485
|
+
* (LD/Statsig `privateAttributes`). Sent to the edge for evaluation, never
|
|
486
|
+
* stored, and stripped from `flags.track(props)`. See
|
|
487
|
+
* {@link FlagsClientBrowserOptions.privateAttributes}.
|
|
488
|
+
*/
|
|
489
|
+
privateAttributes?: string[];
|
|
490
|
+
/**
|
|
491
|
+
* Sticky bucketing (doc 20 §2). ON by default — locks each enrolled unit to
|
|
492
|
+
* its first-assigned variant via the `__se_sticky` cookie. Pass `false` to
|
|
493
|
+
* opt out. See {@link FlagsClientBrowserOptions.stickyBucketing}.
|
|
494
|
+
*/
|
|
495
|
+
stickyBucketing?: boolean;
|
|
336
496
|
}
|
|
337
497
|
/**
|
|
338
498
|
* Initialise the ShipEasy client SDK and wire up lazy devtools.
|
|
@@ -388,9 +548,14 @@ declare const flags: {
|
|
|
388
548
|
* Everything else gates on _mountedAndReady to prevent hydration mismatches on
|
|
389
549
|
* force-static pages where SSR has no flag data.
|
|
390
550
|
*/
|
|
391
|
-
get(name: string): boolean;
|
|
551
|
+
get(name: string, defaultValue?: boolean): boolean;
|
|
552
|
+
/** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
|
|
553
|
+
getDetail(name: string): FlagDetail;
|
|
392
554
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
393
|
-
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P,
|
|
555
|
+
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decodeOrOpts?: ((raw: unknown) => P) | GetExperimentOptions<P>, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
|
|
556
|
+
/** Manually log an exposure for an enrolled experiment. See
|
|
557
|
+
* {@link FlagsClientBrowser.logExposure}. No-op before configure(). */
|
|
558
|
+
logExposure(name: string): void;
|
|
394
559
|
track(eventName: string, props?: Record<string, unknown>): void;
|
|
395
560
|
/**
|
|
396
561
|
* Read a killswitch. Without `switchKey`, returns true when the killswitch is
|
|
@@ -509,4 +674,4 @@ interface I18nFacade {
|
|
|
509
674
|
}
|
|
510
675
|
declare const i18n: I18nFacade;
|
|
511
676
|
|
|
512
|
-
export { type AutoCollectGroups, type BootstrapPayload, type Consequence, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
|
|
677
|
+
export { type AutoCollectGroups, type BootstrapPayload, type Consequence, type ExperimentResult, FLAG_REASONS, type FlagDetail, type FlagReason, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, type GetConfigOptions, type GetExperimentOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
|
package/dist/client/index.d.ts
CHANGED
|
@@ -120,6 +120,47 @@ interface ExperimentResult<P> {
|
|
|
120
120
|
group: string;
|
|
121
121
|
params: P;
|
|
122
122
|
}
|
|
123
|
+
/** Options object form of `getExperiment` — the legacy `decode`/`variants`
|
|
124
|
+
* positional args plus per-call exposure control. */
|
|
125
|
+
interface GetExperimentOptions<P> {
|
|
126
|
+
/** Decode the raw stored params into the typed shape callers want. */
|
|
127
|
+
decode?: (raw: unknown) => P;
|
|
128
|
+
/** Variant-specific param overrides merged on top of group params. */
|
|
129
|
+
variants?: Record<string, Partial<P>>;
|
|
130
|
+
/**
|
|
131
|
+
* Override automatic exposure logging for this read. Defaults to the client's
|
|
132
|
+
* setting (`disableAutoExposure` flips it). `false` reads the variant without
|
|
133
|
+
* logging an exposure — pair with `logExposure(name)` at render time.
|
|
134
|
+
*/
|
|
135
|
+
logExposure?: boolean;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Why a flag evaluated the way it did (LaunchDarkly variationDetail parity).
|
|
139
|
+
* Computed at the client boundary:
|
|
140
|
+
* - CLIENT_NOT_READY — no eval result yet (identify() hasn't resolved)
|
|
141
|
+
* - FLAG_NOT_FOUND — the gate name isn't present in the eval result
|
|
142
|
+
* - OFF — folded into DEFAULT here (the server pre-evaluates the
|
|
143
|
+
* gate's enabled/killed state into a plain boolean, so the browser can't
|
|
144
|
+
* distinguish a disabled gate from one a rule denied)
|
|
145
|
+
* - OVERRIDE — a local override or a ?se_gate_/?se_ks_ URL override
|
|
146
|
+
* decided the value
|
|
147
|
+
* - RULE_MATCH — the gate evaluated true
|
|
148
|
+
* - DEFAULT — the gate evaluated false
|
|
149
|
+
*/
|
|
150
|
+
declare const FLAG_REASONS: readonly ["CLIENT_NOT_READY", "FLAG_NOT_FOUND", "OFF", "OVERRIDE", "RULE_MATCH", "DEFAULT"];
|
|
151
|
+
type FlagReason = (typeof FLAG_REASONS)[number];
|
|
152
|
+
interface FlagDetail {
|
|
153
|
+
value: boolean;
|
|
154
|
+
reason: FlagReason;
|
|
155
|
+
}
|
|
156
|
+
/** Options object form of `getConfig` — keeps the legacy `decode` callback and
|
|
157
|
+
* adds a `defaultValue` returned when the config key is absent. */
|
|
158
|
+
interface GetConfigOptions<T = unknown> {
|
|
159
|
+
/** Decode the raw stored value into the typed shape callers want. */
|
|
160
|
+
decode?: (raw: unknown) => T;
|
|
161
|
+
/** Returned when the config key is absent (not overridden, not in the eval result). */
|
|
162
|
+
defaultValue?: T;
|
|
163
|
+
}
|
|
123
164
|
interface EvalExpResult {
|
|
124
165
|
inExperiment: boolean;
|
|
125
166
|
group: string;
|
|
@@ -135,6 +176,17 @@ interface EvalResponse {
|
|
|
135
176
|
* switch booleans.
|
|
136
177
|
*/
|
|
137
178
|
killswitches?: Record<string, boolean | Record<string, boolean>>;
|
|
179
|
+
/**
|
|
180
|
+
* Newly-assigned sticky entries (doc 20 §2). The worker returns these so the
|
|
181
|
+
* browser can merge them into the `__se_sticky` cookie. Present only when
|
|
182
|
+
* sticky bucketing is on and at least one assignment was made/refreshed.
|
|
183
|
+
*/
|
|
184
|
+
sticky?: Record<string, StickyCookieEntry>;
|
|
185
|
+
}
|
|
186
|
+
/** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
|
|
187
|
+
interface StickyCookieEntry {
|
|
188
|
+
g: string;
|
|
189
|
+
s: string;
|
|
138
190
|
}
|
|
139
191
|
interface AutoCollectGroups {
|
|
140
192
|
vitals: boolean;
|
|
@@ -180,6 +232,36 @@ interface FlagsClientBrowserOptions {
|
|
|
180
232
|
disableTelemetry?: boolean;
|
|
181
233
|
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
182
234
|
telemetryUrl?: string;
|
|
235
|
+
/**
|
|
236
|
+
* Suppress automatic exposure logging in `getExperiment` (Statsig's
|
|
237
|
+
* `disableExposureLogging`). Default false — reading an enrolled variant
|
|
238
|
+
* auto-logs a deduped exposure. When true, no exposure fires unless you call
|
|
239
|
+
* `logExposure(name)` yourself, or pass `{ logExposure: true }` per call.
|
|
240
|
+
*/
|
|
241
|
+
disableAutoExposure?: boolean;
|
|
242
|
+
/**
|
|
243
|
+
* Attribute names usable for targeting but never persisted in analytics
|
|
244
|
+
* (LD/Statsig `privateAttributes`). They are sent to `/sdk/evaluate` under
|
|
245
|
+
* `private_attributes` so the edge can evaluate with them (unavoidable —
|
|
246
|
+
* the edge evaluates), but the worker never stores them, and the listed keys
|
|
247
|
+
* are stripped from any `track(props)` payload.
|
|
248
|
+
*/
|
|
249
|
+
privateAttributes?: string[];
|
|
250
|
+
/**
|
|
251
|
+
* Sticky bucketing (doc 20 §2). ON by default in the browser: a unit's
|
|
252
|
+
* first-assigned variant is locked in the `__se_sticky` cookie so changing an
|
|
253
|
+
* experiment's allocation % or group weights never silently re-buckets
|
|
254
|
+
* enrolled users. Changing the experiment salt is the deliberate reshuffle
|
|
255
|
+
* lever. Pass `false` to opt out (pure deterministic eval).
|
|
256
|
+
*/
|
|
257
|
+
stickyBucketing?: boolean;
|
|
258
|
+
/**
|
|
259
|
+
* Test mode — no network at all. identify()/init are no-ops (never call
|
|
260
|
+
* /sdk/evaluate), track() is a no-op, telemetry is forced off, and the client
|
|
261
|
+
* starts "ready" with an empty eval result. Prefer the
|
|
262
|
+
* {@link FlagsClientBrowser.forTesting} factory over passing this directly.
|
|
263
|
+
*/
|
|
264
|
+
testMode?: boolean;
|
|
183
265
|
}
|
|
184
266
|
declare class FlagsClientBrowser {
|
|
185
267
|
private readonly sdkKey;
|
|
@@ -187,6 +269,9 @@ declare class FlagsClientBrowser {
|
|
|
187
269
|
private readonly autoGuardrails;
|
|
188
270
|
private readonly autoGuardrailGroups;
|
|
189
271
|
private readonly autoCollectAlways;
|
|
272
|
+
private readonly disableAutoExposure;
|
|
273
|
+
private readonly privateAttributes;
|
|
274
|
+
private readonly stickyBucketing;
|
|
190
275
|
private readonly env;
|
|
191
276
|
private evalResult;
|
|
192
277
|
private anonId;
|
|
@@ -198,8 +283,26 @@ declare class FlagsClientBrowser {
|
|
|
198
283
|
private listeners;
|
|
199
284
|
private overrideListenerInstalled;
|
|
200
285
|
private identifySeq;
|
|
286
|
+
private readonly testMode;
|
|
287
|
+
private readonly flagOverrides;
|
|
288
|
+
private readonly configOverrides;
|
|
289
|
+
private readonly experimentOverrides;
|
|
201
290
|
private onOverrideChange;
|
|
202
291
|
constructor(opts: FlagsClientBrowserOptions);
|
|
292
|
+
/**
|
|
293
|
+
* Build a no-network, immediately-usable browser client for tests
|
|
294
|
+
* (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
|
|
295
|
+
* is a no-op, telemetry is disabled, and the client is already ready — seed
|
|
296
|
+
* every entity with overrideFlag/overrideConfig/overrideExperiment. No SDK
|
|
297
|
+
* key required.
|
|
298
|
+
*
|
|
299
|
+
* ```ts
|
|
300
|
+
* const client = FlagsClientBrowser.forTesting();
|
|
301
|
+
* client.overrideFlag("new_checkout", true);
|
|
302
|
+
* client.getFlag("new_checkout"); // true
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
static forTesting(opts?: Partial<FlagsClientBrowserOptions>): FlagsClientBrowser;
|
|
203
306
|
identify(user: User): Promise<void>;
|
|
204
307
|
/**
|
|
205
308
|
* Report a structured error into the errors primitive. Flushes immediately
|
|
@@ -210,9 +313,45 @@ declare class FlagsClientBrowser {
|
|
|
210
313
|
get ready(): boolean;
|
|
211
314
|
private notify;
|
|
212
315
|
initFromBootstrap(data: EvalResponse): void;
|
|
213
|
-
getFlag(name
|
|
316
|
+
/** Force `getFlag(name)` to return `value`, ignoring URL overrides + eval. */
|
|
317
|
+
overrideFlag(name: string, value: boolean): void;
|
|
318
|
+
/** Force `getConfig(name)` to return `value`, ignoring URL overrides + eval. */
|
|
319
|
+
overrideConfig(name: string, value: unknown): void;
|
|
320
|
+
/**
|
|
321
|
+
* Force `getExperiment(name, …)` to return `{ inExperiment: true, group, params }`,
|
|
322
|
+
* ignoring URL overrides, the eval result, and exposure logging.
|
|
323
|
+
*/
|
|
324
|
+
overrideExperiment(name: string, group: string, params: Record<string, unknown>): void;
|
|
325
|
+
/** Remove every programmatic override set via the override* methods. */
|
|
326
|
+
clearOverrides(): void;
|
|
327
|
+
/**
|
|
328
|
+
* Evaluate a gate and report WHY (LaunchDarkly variationDetail parity). A
|
|
329
|
+
* local override OR a ?se_gate_/?se_ks_ URL override short-circuits BEFORE
|
|
330
|
+
* telemetry; otherwise exactly one "gate" beacon is emitted. The server
|
|
331
|
+
* pre-evaluates a gate's enabled/killed state into a plain boolean, so OFF
|
|
332
|
+
* folds into DEFAULT here (the browser can't tell "disabled" from "rule
|
|
333
|
+
* denied").
|
|
334
|
+
*/
|
|
335
|
+
getFlagDetail(name: string): FlagDetail;
|
|
336
|
+
/**
|
|
337
|
+
* Read a feature gate. Returns `defaultValue` ONLY when the gate cannot be
|
|
338
|
+
* evaluated (not ready or flag not found) — never for a gate that legitimately
|
|
339
|
+
* evaluates to false. Plain `getFlag(name)` keeps returning false for a
|
|
340
|
+
* missing flag.
|
|
341
|
+
*/
|
|
342
|
+
getFlag(name: string, defaultValue?: boolean): boolean;
|
|
214
343
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
344
|
+
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
215
345
|
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
|
|
346
|
+
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, opts: GetExperimentOptions<P>): ExperimentResult<P>;
|
|
347
|
+
/**
|
|
348
|
+
* Manually log an exposure for an enrolled experiment (Statsig's
|
|
349
|
+
* `manuallyLogExposure`). Reads the cached eval result; if the visitor is in
|
|
350
|
+
* the experiment, pushes the session-deduped exposure. Pair this with the
|
|
351
|
+
* render of the treatment when reading with `{ logExposure: false }` (or
|
|
352
|
+
* `disableAutoExposure: true`). No-op if the visitor isn't enrolled.
|
|
353
|
+
*/
|
|
354
|
+
logExposure(name: string): void;
|
|
216
355
|
/**
|
|
217
356
|
* Subscribe to state changes — fires after identify() completes and on
|
|
218
357
|
* `se:override:change` events from the devtools overlay. Returns an
|
|
@@ -225,6 +364,8 @@ declare class FlagsClientBrowser {
|
|
|
225
364
|
*/
|
|
226
365
|
installBridge(): ShipeasySdkBridge | null;
|
|
227
366
|
track(eventName: string, props?: Record<string, unknown>): void;
|
|
367
|
+
/** Drop caller-marked private attributes from an outbound props bag. */
|
|
368
|
+
private stripPrivate;
|
|
228
369
|
/**
|
|
229
370
|
* Read a killswitch from the server's evaluated state. Without `switchKey`,
|
|
230
371
|
* returns true when the killswitch is whole-killed. With `switchKey`, returns
|
|
@@ -333,6 +474,25 @@ interface ShipeasyClientConfig {
|
|
|
333
474
|
* counted by Cloudflare's native per-path analytics. Pass `true` to opt out.
|
|
334
475
|
*/
|
|
335
476
|
disableTelemetry?: boolean;
|
|
477
|
+
/**
|
|
478
|
+
* Suppress automatic exposure logging in `flags.getExperiment` (Statsig's
|
|
479
|
+
* `disableExposureLogging`). Default false. When true, call
|
|
480
|
+
* `flags.logExposure(name)` at the treatment's render to log the exposure.
|
|
481
|
+
*/
|
|
482
|
+
disableAutoExposure?: boolean;
|
|
483
|
+
/**
|
|
484
|
+
* Attribute names usable for targeting but never persisted in analytics
|
|
485
|
+
* (LD/Statsig `privateAttributes`). Sent to the edge for evaluation, never
|
|
486
|
+
* stored, and stripped from `flags.track(props)`. See
|
|
487
|
+
* {@link FlagsClientBrowserOptions.privateAttributes}.
|
|
488
|
+
*/
|
|
489
|
+
privateAttributes?: string[];
|
|
490
|
+
/**
|
|
491
|
+
* Sticky bucketing (doc 20 §2). ON by default — locks each enrolled unit to
|
|
492
|
+
* its first-assigned variant via the `__se_sticky` cookie. Pass `false` to
|
|
493
|
+
* opt out. See {@link FlagsClientBrowserOptions.stickyBucketing}.
|
|
494
|
+
*/
|
|
495
|
+
stickyBucketing?: boolean;
|
|
336
496
|
}
|
|
337
497
|
/**
|
|
338
498
|
* Initialise the ShipEasy client SDK and wire up lazy devtools.
|
|
@@ -388,9 +548,14 @@ declare const flags: {
|
|
|
388
548
|
* Everything else gates on _mountedAndReady to prevent hydration mismatches on
|
|
389
549
|
* force-static pages where SSR has no flag data.
|
|
390
550
|
*/
|
|
391
|
-
get(name: string): boolean;
|
|
551
|
+
get(name: string, defaultValue?: boolean): boolean;
|
|
552
|
+
/** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
|
|
553
|
+
getDetail(name: string): FlagDetail;
|
|
392
554
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
393
|
-
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P,
|
|
555
|
+
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decodeOrOpts?: ((raw: unknown) => P) | GetExperimentOptions<P>, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
|
|
556
|
+
/** Manually log an exposure for an enrolled experiment. See
|
|
557
|
+
* {@link FlagsClientBrowser.logExposure}. No-op before configure(). */
|
|
558
|
+
logExposure(name: string): void;
|
|
394
559
|
track(eventName: string, props?: Record<string, unknown>): void;
|
|
395
560
|
/**
|
|
396
561
|
* Read a killswitch. Without `switchKey`, returns true when the killswitch is
|
|
@@ -509,4 +674,4 @@ interface I18nFacade {
|
|
|
509
674
|
}
|
|
510
675
|
declare const i18n: I18nFacade;
|
|
511
676
|
|
|
512
|
-
export { type AutoCollectGroups, type BootstrapPayload, type Consequence, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
|
|
677
|
+
export { type AutoCollectGroups, type BootstrapPayload, type Consequence, type ExperimentResult, FLAG_REASONS, type FlagDetail, type FlagReason, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, type GetConfigOptions, type GetExperimentOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
|