@shipeasy/sdk 4.5.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -13
- package/dist/client/index.d.mts +110 -17
- package/dist/client/index.d.ts +110 -17
- package/dist/client/index.js +170 -73
- package/dist/client/index.mjs +169 -73
- package/dist/server/index.d.mts +170 -16
- package/dist/server/index.d.ts +170 -16
- package/dist/server/index.js +223 -48
- package/dist/server/index.mjs +228 -48
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,12 +69,12 @@ try {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// Non-exception problems — the name is a stable identifier (it participates
|
|
72
|
-
// in the issue fingerprint); variable data goes in .
|
|
72
|
+
// in the issue fingerprint); all variable data goes in .extras():
|
|
73
73
|
if (rows.length > LIMIT) {
|
|
74
74
|
see.Violation("large query")
|
|
75
|
-
.message(`got ${rows.length} rows`)
|
|
76
75
|
.causes_the("search results")
|
|
77
|
-
.to("be trimmed")
|
|
76
|
+
.to("be trimmed")
|
|
77
|
+
.extras({ rows: rows.length });
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// Expected control-flow exceptions: document them, report nothing —
|
|
@@ -82,7 +82,7 @@ if (rows.length > LIMIT) {
|
|
|
82
82
|
try {
|
|
83
83
|
return decodeFoo(blob);
|
|
84
84
|
} catch (e) {
|
|
85
|
-
see.ControlFlowException(e
|
|
85
|
+
see.ControlFlowException(e).because("because it wasn't an encoded Foo");
|
|
86
86
|
return decodeBar(blob);
|
|
87
87
|
}
|
|
88
88
|
```
|
|
@@ -94,16 +94,19 @@ occurrence timeseries. The chain dispatches on the next microtask — no
|
|
|
94
94
|
`fetch` on the server), spam-guarded by a 30s dedup window and a per-session
|
|
95
95
|
cap.
|
|
96
96
|
|
|
97
|
-
The client SDK also auto-captures
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
consequence
|
|
97
|
+
The client SDK also auto-captures **network failures** (fetch network errors +
|
|
98
|
+
5xx) into the same primitive (`autoCollect: { errors }`, on by default) — each
|
|
99
|
+
names a specific endpoint and a specific outcome. It deliberately does **not**
|
|
100
|
+
blanket-report uncaught exceptions or unhandled promise rejections: those carry
|
|
101
|
+
no actionable consequence ("the page hit an error" names the plumbing, not the
|
|
102
|
+
feature). Code that knows the consequence reports it explicitly with `see()` at
|
|
103
|
+
the catch site.
|
|
102
104
|
|
|
103
|
-
**Rules**: if you don't know the consequence, don't catch the exception.
|
|
104
|
-
`see()` then
|
|
105
|
-
|
|
106
|
-
high-cardinality data
|
|
105
|
+
**Rules**: if you don't know the consequence, don't catch the exception. You
|
|
106
|
+
**may** `see()` then re-throw — the re-thrown error links to its inner report
|
|
107
|
+
as a `caused_by` chain instead of double-counting. Never use `see.Violation()`
|
|
108
|
+
for a caught exception (you'd drop the stack). No PII or high-cardinality data
|
|
109
|
+
in extras.
|
|
107
110
|
|
|
108
111
|
## Drop-in `<script>` loader (no bundler)
|
|
109
112
|
|
|
@@ -127,6 +130,164 @@ The loader IIFE is published to a public R2 bucket on every release and
|
|
|
127
130
|
cached for 1y at `loader-vX.Y.Z.js` (immutable) plus a rolling 5-minute
|
|
128
131
|
`loader.js`.
|
|
129
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
|
+
|
|
130
291
|
## Devtools overlay
|
|
131
292
|
|
|
132
293
|
Press `Shift+Alt+S` on any page running the SDK (or append `?se=1` to the
|
package/dist/client/index.d.mts
CHANGED
|
@@ -8,15 +8,13 @@ interface Consequence {
|
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
10
|
* Non-exception problem, built by `violation(name)`. A plain branded object
|
|
11
|
-
* (not an Error subclass)
|
|
12
|
-
*
|
|
11
|
+
* (not an Error subclass). The name is the whole identity — there is no
|
|
12
|
+
* separate message; any variable/context data belongs in `.extras()` on the
|
|
13
|
+
* see chain, never on the violation itself.
|
|
13
14
|
*/
|
|
14
15
|
interface Violation {
|
|
15
16
|
readonly __seViolation: true;
|
|
16
17
|
readonly violationName: string;
|
|
17
|
-
readonly violationMessage?: string;
|
|
18
|
-
/** Attach free-form detail. Variable data goes HERE (or in extras), never in the name. */
|
|
19
|
-
message(msg: string): Violation;
|
|
20
18
|
}
|
|
21
19
|
/**
|
|
22
20
|
* Identity of a problem that see() already reported, carried on the wire as
|
|
@@ -84,9 +82,22 @@ interface SeeChain {
|
|
|
84
82
|
/** camelCase alias of {@link SeeChain.causes_the}. */
|
|
85
83
|
causesThe(subject: string): SeeOutcomeStep;
|
|
86
84
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Violations share the exception consequence grammar exactly — there is no
|
|
87
|
+
* separate `.message()`. Put any variable/context data in `.extras()`.
|
|
88
|
+
*/
|
|
89
|
+
type SeeViolationChain = SeeChain;
|
|
90
|
+
interface SeeControlFlowTail {
|
|
91
|
+
/**
|
|
92
|
+
* Optional debugging context for the expected exception. Kept on the mark for
|
|
93
|
+
* local debugging only (expected exceptions are never reported). Callable
|
|
94
|
+
* repeatedly — keys merge, later wins.
|
|
95
|
+
*/
|
|
96
|
+
extras(extras: SeeExtras): SeeControlFlowTail;
|
|
97
|
+
}
|
|
98
|
+
interface SeeControlFlowChain {
|
|
99
|
+
/** Document why the exception is expected. The reason should start with "because". */
|
|
100
|
+
because(reason: string): SeeControlFlowTail;
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
declare global {
|
|
@@ -109,6 +120,33 @@ interface ExperimentResult<P> {
|
|
|
109
120
|
group: string;
|
|
110
121
|
params: P;
|
|
111
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Why a flag evaluated the way it did (LaunchDarkly variationDetail parity).
|
|
125
|
+
* Computed at the client boundary:
|
|
126
|
+
* - CLIENT_NOT_READY — no eval result yet (identify() hasn't resolved)
|
|
127
|
+
* - FLAG_NOT_FOUND — the gate name isn't present in the eval result
|
|
128
|
+
* - OFF — folded into DEFAULT here (the server pre-evaluates the
|
|
129
|
+
* gate's enabled/killed state into a plain boolean, so the browser can't
|
|
130
|
+
* distinguish a disabled gate from one a rule denied)
|
|
131
|
+
* - OVERRIDE — a local override or a ?se_gate_/?se_ks_ URL override
|
|
132
|
+
* decided the value
|
|
133
|
+
* - RULE_MATCH — the gate evaluated true
|
|
134
|
+
* - DEFAULT — the gate evaluated false
|
|
135
|
+
*/
|
|
136
|
+
declare const FLAG_REASONS: readonly ["CLIENT_NOT_READY", "FLAG_NOT_FOUND", "OFF", "OVERRIDE", "RULE_MATCH", "DEFAULT"];
|
|
137
|
+
type FlagReason = (typeof FLAG_REASONS)[number];
|
|
138
|
+
interface FlagDetail {
|
|
139
|
+
value: boolean;
|
|
140
|
+
reason: FlagReason;
|
|
141
|
+
}
|
|
142
|
+
/** Options object form of `getConfig` — keeps the legacy `decode` callback and
|
|
143
|
+
* adds a `defaultValue` returned when the config key is absent. */
|
|
144
|
+
interface GetConfigOptions<T = unknown> {
|
|
145
|
+
/** Decode the raw stored value into the typed shape callers want. */
|
|
146
|
+
decode?: (raw: unknown) => T;
|
|
147
|
+
/** Returned when the config key is absent (not overridden, not in the eval result). */
|
|
148
|
+
defaultValue?: T;
|
|
149
|
+
}
|
|
112
150
|
interface EvalExpResult {
|
|
113
151
|
inExperiment: boolean;
|
|
114
152
|
group: string;
|
|
@@ -169,6 +207,13 @@ interface FlagsClientBrowserOptions {
|
|
|
169
207
|
disableTelemetry?: boolean;
|
|
170
208
|
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
171
209
|
telemetryUrl?: string;
|
|
210
|
+
/**
|
|
211
|
+
* Test mode — no network at all. identify()/init are no-ops (never call
|
|
212
|
+
* /sdk/evaluate), track() is a no-op, telemetry is forced off, and the client
|
|
213
|
+
* starts "ready" with an empty eval result. Prefer the
|
|
214
|
+
* {@link FlagsClientBrowser.forTesting} factory over passing this directly.
|
|
215
|
+
*/
|
|
216
|
+
testMode?: boolean;
|
|
172
217
|
}
|
|
173
218
|
declare class FlagsClientBrowser {
|
|
174
219
|
private readonly sdkKey;
|
|
@@ -187,8 +232,26 @@ declare class FlagsClientBrowser {
|
|
|
187
232
|
private listeners;
|
|
188
233
|
private overrideListenerInstalled;
|
|
189
234
|
private identifySeq;
|
|
235
|
+
private readonly testMode;
|
|
236
|
+
private readonly flagOverrides;
|
|
237
|
+
private readonly configOverrides;
|
|
238
|
+
private readonly experimentOverrides;
|
|
190
239
|
private onOverrideChange;
|
|
191
240
|
constructor(opts: FlagsClientBrowserOptions);
|
|
241
|
+
/**
|
|
242
|
+
* Build a no-network, immediately-usable browser client for tests
|
|
243
|
+
* (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
|
|
244
|
+
* is a no-op, telemetry is disabled, and the client is already ready — seed
|
|
245
|
+
* every entity with overrideFlag/overrideConfig/overrideExperiment. No SDK
|
|
246
|
+
* key required.
|
|
247
|
+
*
|
|
248
|
+
* ```ts
|
|
249
|
+
* const client = FlagsClientBrowser.forTesting();
|
|
250
|
+
* client.overrideFlag("new_checkout", true);
|
|
251
|
+
* client.getFlag("new_checkout"); // true
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
static forTesting(opts?: Partial<FlagsClientBrowserOptions>): FlagsClientBrowser;
|
|
192
255
|
identify(user: User): Promise<void>;
|
|
193
256
|
/**
|
|
194
257
|
* Report a structured error into the errors primitive. Flushes immediately
|
|
@@ -199,8 +262,35 @@ declare class FlagsClientBrowser {
|
|
|
199
262
|
get ready(): boolean;
|
|
200
263
|
private notify;
|
|
201
264
|
initFromBootstrap(data: EvalResponse): void;
|
|
202
|
-
getFlag(name
|
|
265
|
+
/** Force `getFlag(name)` to return `value`, ignoring URL overrides + eval. */
|
|
266
|
+
overrideFlag(name: string, value: boolean): void;
|
|
267
|
+
/** Force `getConfig(name)` to return `value`, ignoring URL overrides + eval. */
|
|
268
|
+
overrideConfig(name: string, value: unknown): void;
|
|
269
|
+
/**
|
|
270
|
+
* Force `getExperiment(name, …)` to return `{ inExperiment: true, group, params }`,
|
|
271
|
+
* ignoring URL overrides, the eval result, and exposure logging.
|
|
272
|
+
*/
|
|
273
|
+
overrideExperiment(name: string, group: string, params: Record<string, unknown>): void;
|
|
274
|
+
/** Remove every programmatic override set via the override* methods. */
|
|
275
|
+
clearOverrides(): void;
|
|
276
|
+
/**
|
|
277
|
+
* Evaluate a gate and report WHY (LaunchDarkly variationDetail parity). A
|
|
278
|
+
* local override OR a ?se_gate_/?se_ks_ URL override short-circuits BEFORE
|
|
279
|
+
* telemetry; otherwise exactly one "gate" beacon is emitted. The server
|
|
280
|
+
* pre-evaluates a gate's enabled/killed state into a plain boolean, so OFF
|
|
281
|
+
* folds into DEFAULT here (the browser can't tell "disabled" from "rule
|
|
282
|
+
* denied").
|
|
283
|
+
*/
|
|
284
|
+
getFlagDetail(name: string): FlagDetail;
|
|
285
|
+
/**
|
|
286
|
+
* Read a feature gate. Returns `defaultValue` ONLY when the gate cannot be
|
|
287
|
+
* evaluated (not ready or flag not found) — never for a gate that legitimately
|
|
288
|
+
* evaluates to false. Plain `getFlag(name)` keeps returning false for a
|
|
289
|
+
* missing flag.
|
|
290
|
+
*/
|
|
291
|
+
getFlag(name: string, defaultValue?: boolean): boolean;
|
|
203
292
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
293
|
+
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
204
294
|
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
|
|
205
295
|
/**
|
|
206
296
|
* Subscribe to state changes — fires after identify() completes and on
|
|
@@ -377,7 +467,9 @@ declare const flags: {
|
|
|
377
467
|
* Everything else gates on _mountedAndReady to prevent hydration mismatches on
|
|
378
468
|
* force-static pages where SSR has no flag data.
|
|
379
469
|
*/
|
|
380
|
-
get(name: string): boolean;
|
|
470
|
+
get(name: string, defaultValue?: boolean): boolean;
|
|
471
|
+
/** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
|
|
472
|
+
getDetail(name: string): FlagDetail;
|
|
381
473
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
382
474
|
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
|
|
383
475
|
track(eventName: string, props?: Record<string, unknown>): void;
|
|
@@ -430,28 +522,29 @@ interface SeeApi {
|
|
|
430
522
|
/**
|
|
431
523
|
* Report a non-exception problem. Prefer passing a caught Error to `see()`
|
|
432
524
|
* when one exists. The name is a stable identifier (it participates in the
|
|
433
|
-
* issue fingerprint) — variable data goes in `.
|
|
525
|
+
* issue fingerprint) — variable data goes in `.extras()`, never the name.
|
|
434
526
|
*
|
|
435
527
|
* ```ts
|
|
436
528
|
* if (rows.length > LIMIT) {
|
|
437
|
-
* see.Violation("large query")
|
|
438
|
-
* .causes_the("search results").to("be trimmed");
|
|
529
|
+
* see.Violation("large query")
|
|
530
|
+
* .causes_the("search results").to("be trimmed").extras({ rows: rows.length });
|
|
439
531
|
* }
|
|
440
532
|
* ```
|
|
441
533
|
*/
|
|
442
534
|
Violation(name: string): SeeViolationChain;
|
|
443
535
|
/**
|
|
444
536
|
* Mark an exception as expected control flow — auto-capture skips it and
|
|
445
|
-
* nothing is reported.
|
|
537
|
+
* nothing is reported. Say why with `.because()` (reason should start with
|
|
538
|
+
* "because"); attach optional debug context with `.extras()`.
|
|
446
539
|
*
|
|
447
540
|
* ```ts
|
|
448
541
|
* } catch (e) {
|
|
449
|
-
* see.ControlFlowException(e
|
|
542
|
+
* see.ControlFlowException(e).because("because the blob wasn't an encoded Foo");
|
|
450
543
|
* return decodeAsBar(blob);
|
|
451
544
|
* }
|
|
452
545
|
* ```
|
|
453
546
|
*/
|
|
454
|
-
ControlFlowException(err: unknown
|
|
547
|
+
ControlFlowException(err: unknown): SeeControlFlowChain;
|
|
455
548
|
}
|
|
456
549
|
/**
|
|
457
550
|
* Structured error reporter — the whole grammar hangs off this one import.
|
|
@@ -497,4 +590,4 @@ interface I18nFacade {
|
|
|
497
590
|
}
|
|
498
591
|
declare const i18n: I18nFacade;
|
|
499
592
|
|
|
500
|
-
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 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 };
|
|
593
|
+
export { type AutoCollectGroups, type BootstrapPayload, type Consequence, type ExperimentResult, FLAG_REASONS, type FlagDetail, type FlagReason, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, type GetConfigOptions, 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
|
@@ -8,15 +8,13 @@ interface Consequence {
|
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
10
|
* Non-exception problem, built by `violation(name)`. A plain branded object
|
|
11
|
-
* (not an Error subclass)
|
|
12
|
-
*
|
|
11
|
+
* (not an Error subclass). The name is the whole identity — there is no
|
|
12
|
+
* separate message; any variable/context data belongs in `.extras()` on the
|
|
13
|
+
* see chain, never on the violation itself.
|
|
13
14
|
*/
|
|
14
15
|
interface Violation {
|
|
15
16
|
readonly __seViolation: true;
|
|
16
17
|
readonly violationName: string;
|
|
17
|
-
readonly violationMessage?: string;
|
|
18
|
-
/** Attach free-form detail. Variable data goes HERE (or in extras), never in the name. */
|
|
19
|
-
message(msg: string): Violation;
|
|
20
18
|
}
|
|
21
19
|
/**
|
|
22
20
|
* Identity of a problem that see() already reported, carried on the wire as
|
|
@@ -84,9 +82,22 @@ interface SeeChain {
|
|
|
84
82
|
/** camelCase alias of {@link SeeChain.causes_the}. */
|
|
85
83
|
causesThe(subject: string): SeeOutcomeStep;
|
|
86
84
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Violations share the exception consequence grammar exactly — there is no
|
|
87
|
+
* separate `.message()`. Put any variable/context data in `.extras()`.
|
|
88
|
+
*/
|
|
89
|
+
type SeeViolationChain = SeeChain;
|
|
90
|
+
interface SeeControlFlowTail {
|
|
91
|
+
/**
|
|
92
|
+
* Optional debugging context for the expected exception. Kept on the mark for
|
|
93
|
+
* local debugging only (expected exceptions are never reported). Callable
|
|
94
|
+
* repeatedly — keys merge, later wins.
|
|
95
|
+
*/
|
|
96
|
+
extras(extras: SeeExtras): SeeControlFlowTail;
|
|
97
|
+
}
|
|
98
|
+
interface SeeControlFlowChain {
|
|
99
|
+
/** Document why the exception is expected. The reason should start with "because". */
|
|
100
|
+
because(reason: string): SeeControlFlowTail;
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
declare global {
|
|
@@ -109,6 +120,33 @@ interface ExperimentResult<P> {
|
|
|
109
120
|
group: string;
|
|
110
121
|
params: P;
|
|
111
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Why a flag evaluated the way it did (LaunchDarkly variationDetail parity).
|
|
125
|
+
* Computed at the client boundary:
|
|
126
|
+
* - CLIENT_NOT_READY — no eval result yet (identify() hasn't resolved)
|
|
127
|
+
* - FLAG_NOT_FOUND — the gate name isn't present in the eval result
|
|
128
|
+
* - OFF — folded into DEFAULT here (the server pre-evaluates the
|
|
129
|
+
* gate's enabled/killed state into a plain boolean, so the browser can't
|
|
130
|
+
* distinguish a disabled gate from one a rule denied)
|
|
131
|
+
* - OVERRIDE — a local override or a ?se_gate_/?se_ks_ URL override
|
|
132
|
+
* decided the value
|
|
133
|
+
* - RULE_MATCH — the gate evaluated true
|
|
134
|
+
* - DEFAULT — the gate evaluated false
|
|
135
|
+
*/
|
|
136
|
+
declare const FLAG_REASONS: readonly ["CLIENT_NOT_READY", "FLAG_NOT_FOUND", "OFF", "OVERRIDE", "RULE_MATCH", "DEFAULT"];
|
|
137
|
+
type FlagReason = (typeof FLAG_REASONS)[number];
|
|
138
|
+
interface FlagDetail {
|
|
139
|
+
value: boolean;
|
|
140
|
+
reason: FlagReason;
|
|
141
|
+
}
|
|
142
|
+
/** Options object form of `getConfig` — keeps the legacy `decode` callback and
|
|
143
|
+
* adds a `defaultValue` returned when the config key is absent. */
|
|
144
|
+
interface GetConfigOptions<T = unknown> {
|
|
145
|
+
/** Decode the raw stored value into the typed shape callers want. */
|
|
146
|
+
decode?: (raw: unknown) => T;
|
|
147
|
+
/** Returned when the config key is absent (not overridden, not in the eval result). */
|
|
148
|
+
defaultValue?: T;
|
|
149
|
+
}
|
|
112
150
|
interface EvalExpResult {
|
|
113
151
|
inExperiment: boolean;
|
|
114
152
|
group: string;
|
|
@@ -169,6 +207,13 @@ interface FlagsClientBrowserOptions {
|
|
|
169
207
|
disableTelemetry?: boolean;
|
|
170
208
|
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
171
209
|
telemetryUrl?: string;
|
|
210
|
+
/**
|
|
211
|
+
* Test mode — no network at all. identify()/init are no-ops (never call
|
|
212
|
+
* /sdk/evaluate), track() is a no-op, telemetry is forced off, and the client
|
|
213
|
+
* starts "ready" with an empty eval result. Prefer the
|
|
214
|
+
* {@link FlagsClientBrowser.forTesting} factory over passing this directly.
|
|
215
|
+
*/
|
|
216
|
+
testMode?: boolean;
|
|
172
217
|
}
|
|
173
218
|
declare class FlagsClientBrowser {
|
|
174
219
|
private readonly sdkKey;
|
|
@@ -187,8 +232,26 @@ declare class FlagsClientBrowser {
|
|
|
187
232
|
private listeners;
|
|
188
233
|
private overrideListenerInstalled;
|
|
189
234
|
private identifySeq;
|
|
235
|
+
private readonly testMode;
|
|
236
|
+
private readonly flagOverrides;
|
|
237
|
+
private readonly configOverrides;
|
|
238
|
+
private readonly experimentOverrides;
|
|
190
239
|
private onOverrideChange;
|
|
191
240
|
constructor(opts: FlagsClientBrowserOptions);
|
|
241
|
+
/**
|
|
242
|
+
* Build a no-network, immediately-usable browser client for tests
|
|
243
|
+
* (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
|
|
244
|
+
* is a no-op, telemetry is disabled, and the client is already ready — seed
|
|
245
|
+
* every entity with overrideFlag/overrideConfig/overrideExperiment. No SDK
|
|
246
|
+
* key required.
|
|
247
|
+
*
|
|
248
|
+
* ```ts
|
|
249
|
+
* const client = FlagsClientBrowser.forTesting();
|
|
250
|
+
* client.overrideFlag("new_checkout", true);
|
|
251
|
+
* client.getFlag("new_checkout"); // true
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
static forTesting(opts?: Partial<FlagsClientBrowserOptions>): FlagsClientBrowser;
|
|
192
255
|
identify(user: User): Promise<void>;
|
|
193
256
|
/**
|
|
194
257
|
* Report a structured error into the errors primitive. Flushes immediately
|
|
@@ -199,8 +262,35 @@ declare class FlagsClientBrowser {
|
|
|
199
262
|
get ready(): boolean;
|
|
200
263
|
private notify;
|
|
201
264
|
initFromBootstrap(data: EvalResponse): void;
|
|
202
|
-
getFlag(name
|
|
265
|
+
/** Force `getFlag(name)` to return `value`, ignoring URL overrides + eval. */
|
|
266
|
+
overrideFlag(name: string, value: boolean): void;
|
|
267
|
+
/** Force `getConfig(name)` to return `value`, ignoring URL overrides + eval. */
|
|
268
|
+
overrideConfig(name: string, value: unknown): void;
|
|
269
|
+
/**
|
|
270
|
+
* Force `getExperiment(name, …)` to return `{ inExperiment: true, group, params }`,
|
|
271
|
+
* ignoring URL overrides, the eval result, and exposure logging.
|
|
272
|
+
*/
|
|
273
|
+
overrideExperiment(name: string, group: string, params: Record<string, unknown>): void;
|
|
274
|
+
/** Remove every programmatic override set via the override* methods. */
|
|
275
|
+
clearOverrides(): void;
|
|
276
|
+
/**
|
|
277
|
+
* Evaluate a gate and report WHY (LaunchDarkly variationDetail parity). A
|
|
278
|
+
* local override OR a ?se_gate_/?se_ks_ URL override short-circuits BEFORE
|
|
279
|
+
* telemetry; otherwise exactly one "gate" beacon is emitted. The server
|
|
280
|
+
* pre-evaluates a gate's enabled/killed state into a plain boolean, so OFF
|
|
281
|
+
* folds into DEFAULT here (the browser can't tell "disabled" from "rule
|
|
282
|
+
* denied").
|
|
283
|
+
*/
|
|
284
|
+
getFlagDetail(name: string): FlagDetail;
|
|
285
|
+
/**
|
|
286
|
+
* Read a feature gate. Returns `defaultValue` ONLY when the gate cannot be
|
|
287
|
+
* evaluated (not ready or flag not found) — never for a gate that legitimately
|
|
288
|
+
* evaluates to false. Plain `getFlag(name)` keeps returning false for a
|
|
289
|
+
* missing flag.
|
|
290
|
+
*/
|
|
291
|
+
getFlag(name: string, defaultValue?: boolean): boolean;
|
|
203
292
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
293
|
+
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
204
294
|
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
|
|
205
295
|
/**
|
|
206
296
|
* Subscribe to state changes — fires after identify() completes and on
|
|
@@ -377,7 +467,9 @@ declare const flags: {
|
|
|
377
467
|
* Everything else gates on _mountedAndReady to prevent hydration mismatches on
|
|
378
468
|
* force-static pages where SSR has no flag data.
|
|
379
469
|
*/
|
|
380
|
-
get(name: string): boolean;
|
|
470
|
+
get(name: string, defaultValue?: boolean): boolean;
|
|
471
|
+
/** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
|
|
472
|
+
getDetail(name: string): FlagDetail;
|
|
381
473
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
382
474
|
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
|
|
383
475
|
track(eventName: string, props?: Record<string, unknown>): void;
|
|
@@ -430,28 +522,29 @@ interface SeeApi {
|
|
|
430
522
|
/**
|
|
431
523
|
* Report a non-exception problem. Prefer passing a caught Error to `see()`
|
|
432
524
|
* when one exists. The name is a stable identifier (it participates in the
|
|
433
|
-
* issue fingerprint) — variable data goes in `.
|
|
525
|
+
* issue fingerprint) — variable data goes in `.extras()`, never the name.
|
|
434
526
|
*
|
|
435
527
|
* ```ts
|
|
436
528
|
* if (rows.length > LIMIT) {
|
|
437
|
-
* see.Violation("large query")
|
|
438
|
-
* .causes_the("search results").to("be trimmed");
|
|
529
|
+
* see.Violation("large query")
|
|
530
|
+
* .causes_the("search results").to("be trimmed").extras({ rows: rows.length });
|
|
439
531
|
* }
|
|
440
532
|
* ```
|
|
441
533
|
*/
|
|
442
534
|
Violation(name: string): SeeViolationChain;
|
|
443
535
|
/**
|
|
444
536
|
* Mark an exception as expected control flow — auto-capture skips it and
|
|
445
|
-
* nothing is reported.
|
|
537
|
+
* nothing is reported. Say why with `.because()` (reason should start with
|
|
538
|
+
* "because"); attach optional debug context with `.extras()`.
|
|
446
539
|
*
|
|
447
540
|
* ```ts
|
|
448
541
|
* } catch (e) {
|
|
449
|
-
* see.ControlFlowException(e
|
|
542
|
+
* see.ControlFlowException(e).because("because the blob wasn't an encoded Foo");
|
|
450
543
|
* return decodeAsBar(blob);
|
|
451
544
|
* }
|
|
452
545
|
* ```
|
|
453
546
|
*/
|
|
454
|
-
ControlFlowException(err: unknown
|
|
547
|
+
ControlFlowException(err: unknown): SeeControlFlowChain;
|
|
455
548
|
}
|
|
456
549
|
/**
|
|
457
550
|
* Structured error reporter — the whole grammar hangs off this one import.
|
|
@@ -497,4 +590,4 @@ interface I18nFacade {
|
|
|
497
590
|
}
|
|
498
591
|
declare const i18n: I18nFacade;
|
|
499
592
|
|
|
500
|
-
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 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 };
|
|
593
|
+
export { type AutoCollectGroups, type BootstrapPayload, type Consequence, type ExperimentResult, FLAG_REASONS, type FlagDetail, type FlagReason, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, type GetConfigOptions, 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 };
|