@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/dist/server/index.d.ts
CHANGED
|
@@ -10,15 +10,13 @@ interface Consequence {
|
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
12
|
* Non-exception problem, built by `violation(name)`. A plain branded object
|
|
13
|
-
* (not an Error subclass)
|
|
14
|
-
*
|
|
13
|
+
* (not an Error subclass). The name is the whole identity — there is no
|
|
14
|
+
* separate message; any variable/context data belongs in `.extras()` on the
|
|
15
|
+
* see chain, never on the violation itself.
|
|
15
16
|
*/
|
|
16
17
|
interface Violation {
|
|
17
18
|
readonly __seViolation: true;
|
|
18
19
|
readonly violationName: string;
|
|
19
|
-
readonly violationMessage?: string;
|
|
20
|
-
/** Attach free-form detail. Variable data goes HERE (or in extras), never in the name. */
|
|
21
|
-
message(msg: string): Violation;
|
|
22
20
|
}
|
|
23
21
|
/**
|
|
24
22
|
* Identity of a problem that see() already reported, carried on the wire as
|
|
@@ -87,12 +85,33 @@ interface SeeChain {
|
|
|
87
85
|
/** camelCase alias of {@link SeeChain.causes_the}. */
|
|
88
86
|
causesThe(subject: string): SeeOutcomeStep;
|
|
89
87
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Violations share the exception consequence grammar exactly — there is no
|
|
90
|
+
* separate `.message()`. Put any variable/context data in `.extras()`.
|
|
91
|
+
*/
|
|
92
|
+
type SeeViolationChain = SeeChain;
|
|
93
|
+
interface SeeControlFlowTail {
|
|
94
|
+
/**
|
|
95
|
+
* Optional debugging context for the expected exception. Kept on the mark for
|
|
96
|
+
* local debugging only (expected exceptions are never reported). Callable
|
|
97
|
+
* repeatedly — keys merge, later wins.
|
|
98
|
+
*/
|
|
99
|
+
extras(extras: SeeExtras): SeeControlFlowTail;
|
|
100
|
+
}
|
|
101
|
+
interface SeeControlFlowChain {
|
|
102
|
+
/** Document why the exception is expected. The reason should start with "because". */
|
|
103
|
+
because(reason: string): SeeControlFlowTail;
|
|
93
104
|
}
|
|
94
105
|
|
|
95
106
|
declare const version = "4.0.0";
|
|
107
|
+
/**
|
|
108
|
+
* @internal Exported ONLY so the cross-language eval-parity golden-vector test
|
|
109
|
+
* (src/__tests__/eval-vectors.test.ts) can assert the raw unsigned-32-bit hash
|
|
110
|
+
* against the canonical fixture. Not part of the public SDK surface — do not
|
|
111
|
+
* rely on it in product code; the bucketing contract lives behind getFlag /
|
|
112
|
+
* getExperiment. Name-prefixed with `_` to signal "test seam, not API".
|
|
113
|
+
*/
|
|
114
|
+
declare function _murmur3ForTests(key: string): number;
|
|
96
115
|
interface User {
|
|
97
116
|
user_id?: string;
|
|
98
117
|
anonymous_id?: string;
|
|
@@ -103,6 +122,30 @@ interface ExperimentResult<P> {
|
|
|
103
122
|
group: string;
|
|
104
123
|
params: P;
|
|
105
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Why a flag evaluated the way it did (LaunchDarkly variationDetail parity).
|
|
127
|
+
* Computed at the client boundary, never inside the canonical eval:
|
|
128
|
+
* - CLIENT_NOT_READY — no rules blob loaded yet (init()/initOnce() pending)
|
|
129
|
+
* - FLAG_NOT_FOUND — the gate name isn't present in the loaded blob
|
|
130
|
+
* - OFF — the gate exists but is disabled / killed
|
|
131
|
+
* - OVERRIDE — a local override (overrideFlag) decided the value
|
|
132
|
+
* - RULE_MATCH — the gate evaluated true (rules + rollout passed)
|
|
133
|
+
* - DEFAULT — the gate evaluated false (a rule or the rollout denied)
|
|
134
|
+
*/
|
|
135
|
+
declare const FLAG_REASONS: readonly ["CLIENT_NOT_READY", "FLAG_NOT_FOUND", "OFF", "OVERRIDE", "RULE_MATCH", "DEFAULT"];
|
|
136
|
+
type FlagReason = (typeof FLAG_REASONS)[number];
|
|
137
|
+
interface FlagDetail {
|
|
138
|
+
value: boolean;
|
|
139
|
+
reason: FlagReason;
|
|
140
|
+
}
|
|
141
|
+
/** Options object form of `getConfig` — keeps the legacy `decode` callback and
|
|
142
|
+
* adds a `defaultValue` returned when the config key is absent. */
|
|
143
|
+
interface GetConfigOptions<T = unknown> {
|
|
144
|
+
/** Decode the raw stored value into the typed shape callers want. */
|
|
145
|
+
decode?: (raw: unknown) => T;
|
|
146
|
+
/** Returned when the config key is absent (not overridden, not in the blob). */
|
|
147
|
+
defaultValue?: T;
|
|
148
|
+
}
|
|
106
149
|
interface GateRule {
|
|
107
150
|
attr: string;
|
|
108
151
|
op: "eq" | "neq" | "in" | "not_in" | "gt" | "gte" | "lt" | "lte" | "contains" | "regex";
|
|
@@ -115,10 +158,27 @@ interface Gate {
|
|
|
115
158
|
enabled: 0 | 1 | boolean;
|
|
116
159
|
killswitch?: 0 | 1 | boolean;
|
|
117
160
|
}
|
|
161
|
+
interface ExperimentGroup {
|
|
162
|
+
name: string;
|
|
163
|
+
weight: number;
|
|
164
|
+
params: Record<string, unknown>;
|
|
165
|
+
}
|
|
166
|
+
interface Experiment {
|
|
167
|
+
universe: string;
|
|
168
|
+
targetingGate?: string | null;
|
|
169
|
+
allocationPct: number;
|
|
170
|
+
salt: string;
|
|
171
|
+
groups: ExperimentGroup[];
|
|
172
|
+
status: "draft" | "running" | "stopped" | "archived";
|
|
173
|
+
}
|
|
174
|
+
interface Universe {
|
|
175
|
+
holdout_range: [number, number] | null;
|
|
176
|
+
}
|
|
118
177
|
interface Killswitch {
|
|
119
178
|
killed: 0 | 1 | boolean;
|
|
120
179
|
switches?: Record<string, 0 | 1 | boolean>;
|
|
121
180
|
}
|
|
181
|
+
/** Body of `GET /sdk/flags` — the snapshot's `flags` field. See {@link FlagsClient.fromSnapshot}. */
|
|
122
182
|
interface FlagsBlob {
|
|
123
183
|
version: string;
|
|
124
184
|
plan: string;
|
|
@@ -128,6 +188,12 @@ interface FlagsBlob {
|
|
|
128
188
|
}>;
|
|
129
189
|
killswitches: Record<string, Killswitch>;
|
|
130
190
|
}
|
|
191
|
+
/** Body of `GET /sdk/experiments` — the snapshot's `experiments` field. See {@link FlagsClient.fromSnapshot}. */
|
|
192
|
+
interface ExpsBlob {
|
|
193
|
+
version: string;
|
|
194
|
+
universes: Record<string, Universe>;
|
|
195
|
+
experiments: Record<string, Experiment>;
|
|
196
|
+
}
|
|
131
197
|
interface BootstrapPayload {
|
|
132
198
|
flags: Record<string, boolean>;
|
|
133
199
|
configs: Record<string, unknown>;
|
|
@@ -157,6 +223,13 @@ interface FlagsClientOptions {
|
|
|
157
223
|
disableTelemetry?: boolean;
|
|
158
224
|
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
159
225
|
telemetryUrl?: string;
|
|
226
|
+
/**
|
|
227
|
+
* Test mode — no network at all. init()/initOnce() are no-ops (never fetch),
|
|
228
|
+
* track() is a no-op, telemetry is forced off, and the client starts
|
|
229
|
+
* "initialized" with an empty blob. Prefer the {@link FlagsClient.forTesting}
|
|
230
|
+
* factory over passing this directly.
|
|
231
|
+
*/
|
|
232
|
+
testMode?: boolean;
|
|
160
233
|
}
|
|
161
234
|
declare class FlagsClient {
|
|
162
235
|
private readonly apiKey;
|
|
@@ -171,16 +244,89 @@ declare class FlagsClient {
|
|
|
171
244
|
private pollInterval;
|
|
172
245
|
private timer;
|
|
173
246
|
private initialized;
|
|
247
|
+
private readonly testMode;
|
|
248
|
+
private readonly flagOverrides;
|
|
249
|
+
private readonly configOverrides;
|
|
250
|
+
private readonly experimentOverrides;
|
|
251
|
+
private readonly changeListeners;
|
|
174
252
|
constructor(opts: FlagsClientOptions);
|
|
253
|
+
/**
|
|
254
|
+
* Build a no-network, immediately-usable client for tests (Statsig-style).
|
|
255
|
+
* init()/initOnce() are no-ops (never fetch), track() is a no-op, telemetry
|
|
256
|
+
* is disabled, and the client is already "initialized" — seed every entity
|
|
257
|
+
* with overrideFlag/overrideConfig/overrideExperiment. No SDK key required.
|
|
258
|
+
*
|
|
259
|
+
* ```ts
|
|
260
|
+
* const client = FlagsClient.forTesting();
|
|
261
|
+
* client.overrideFlag("new_checkout", true);
|
|
262
|
+
* client.getFlag("new_checkout", { user_id: "u1" }); // true
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
static forTesting(opts?: Partial<FlagsClientOptions>): FlagsClient;
|
|
266
|
+
/**
|
|
267
|
+
* Build a fully OFFLINE client from a pre-captured snapshot — no network ever.
|
|
268
|
+
* Reuses the test-mode plumbing (init()/initOnce()/track() are no-ops,
|
|
269
|
+
* telemetry off) but, unlike forTesting(), seeds the REAL flags + experiments
|
|
270
|
+
* blobs so evaluations run the canonical eval against the snapshot. Local
|
|
271
|
+
* overrides still apply on top.
|
|
272
|
+
*
|
|
273
|
+
* Snapshot shape mirrors the wire bodies:
|
|
274
|
+
* `{ flags: <GET /sdk/flags body>, experiments: <GET /sdk/experiments body> }`.
|
|
275
|
+
*/
|
|
276
|
+
static fromSnapshot(snapshot: {
|
|
277
|
+
flags: FlagsBlob;
|
|
278
|
+
experiments: ExpsBlob;
|
|
279
|
+
}): FlagsClient;
|
|
280
|
+
/**
|
|
281
|
+
* Build a fully OFFLINE client from a snapshot JSON file on disk (Node only —
|
|
282
|
+
* not available in the browser entrypoint). The file must contain
|
|
283
|
+
* `{ "flags": <GET /sdk/flags body>, "experiments": <GET /sdk/experiments body> }`.
|
|
284
|
+
* See {@link FlagsClient.fromSnapshot}.
|
|
285
|
+
*/
|
|
286
|
+
static fromFile(path: string): FlagsClient;
|
|
175
287
|
init(): Promise<void>;
|
|
176
288
|
initOnce(): Promise<void>;
|
|
289
|
+
/** Force `getFlag(name, …)` to return `value`, ignoring the fetched gate. */
|
|
290
|
+
overrideFlag(name: string, value: boolean): void;
|
|
291
|
+
/** Force `getConfig(name)` to return `value`, ignoring the fetched config. */
|
|
292
|
+
overrideConfig(name: string, value: unknown): void;
|
|
293
|
+
/**
|
|
294
|
+
* Force `getExperiment(name, …)` to return `{ inExperiment: true, group, params }`,
|
|
295
|
+
* ignoring allocation, holdouts, and targeting.
|
|
296
|
+
*/
|
|
297
|
+
overrideExperiment(name: string, group: string, params: Record<string, unknown>): void;
|
|
298
|
+
/** Remove every programmatic override set via the override* methods. */
|
|
299
|
+
clearOverrides(): void;
|
|
177
300
|
destroy(): void;
|
|
301
|
+
/**
|
|
302
|
+
* Subscribe to data-change notifications. The listener fires after a
|
|
303
|
+
* background poll fetch returns NEW data (200, not 304) — i.e. after the
|
|
304
|
+
* cached blob is updated. Never fires in testMode/offline (no polling).
|
|
305
|
+
* Returns an unsubscribe function.
|
|
306
|
+
*/
|
|
307
|
+
onChange(listener: () => void): () => void;
|
|
308
|
+
private notifyChange;
|
|
178
309
|
private startPoll;
|
|
179
310
|
private fetchAll;
|
|
180
311
|
private fetchFlags;
|
|
181
312
|
private fetchExps;
|
|
182
|
-
|
|
313
|
+
/**
|
|
314
|
+
* Evaluate a gate and report WHY (LaunchDarkly variationDetail parity). The
|
|
315
|
+
* reason is computed entirely at this boundary — the canonical eval
|
|
316
|
+
* (evalGateInternal) is untouched. A local override short-circuits BEFORE
|
|
317
|
+
* telemetry, exactly like getFlag's override path; otherwise exactly one
|
|
318
|
+
* "gate" beacon is emitted.
|
|
319
|
+
*/
|
|
320
|
+
getFlagDetail(name: string, user: User): FlagDetail;
|
|
321
|
+
/**
|
|
322
|
+
* Read a feature gate. Returns `defaultValue` ONLY when the gate cannot be
|
|
323
|
+
* evaluated (client not initialized or flag not found) — never for a gate that
|
|
324
|
+
* legitimately evaluates to false. Plain `getFlag(name, user)` keeps returning
|
|
325
|
+
* false for a missing flag.
|
|
326
|
+
*/
|
|
327
|
+
getFlag(name: string, user: User, defaultValue?: boolean): boolean;
|
|
183
328
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
329
|
+
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
184
330
|
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
185
331
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
186
332
|
/**
|
|
@@ -327,8 +473,10 @@ declare const flags: {
|
|
|
327
473
|
initOnce(): Promise<void>;
|
|
328
474
|
/** Stop background timers. Safe to call repeatedly. */
|
|
329
475
|
destroy(): void;
|
|
330
|
-
get(name: string, user: User): boolean;
|
|
331
|
-
|
|
476
|
+
get(name: string, user: User, defaultValue?: boolean): boolean;
|
|
477
|
+
/** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
|
|
478
|
+
getDetail(name: string, user: User): FlagDetail;
|
|
479
|
+
getConfig<T = unknown>(name: string, decodeOrOpts?: ((raw: unknown) => T) | GetConfigOptions<T>): T | undefined;
|
|
332
480
|
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
333
481
|
/**
|
|
334
482
|
* Read a killswitch. Without `switchKey`, returns true when the whole
|
|
@@ -367,20 +515,26 @@ interface SeeApi {
|
|
|
367
515
|
/**
|
|
368
516
|
* Report a non-exception problem. Prefer passing a caught Error to `see()`
|
|
369
517
|
* when one exists. The name is a stable identifier (it participates in the
|
|
370
|
-
* issue fingerprint) — variable data goes in `.
|
|
518
|
+
* issue fingerprint) — variable data goes in `.extras()`, never the name.
|
|
371
519
|
*
|
|
372
520
|
* ```ts
|
|
373
521
|
* if (results.length > LIMIT) {
|
|
374
|
-
* see.Violation("large query")
|
|
522
|
+
* see.Violation("large query")
|
|
523
|
+
* .causes_the("search results").to("be trimmed").extras({ rows: results.length });
|
|
375
524
|
* }
|
|
376
525
|
* ```
|
|
377
526
|
*/
|
|
378
527
|
Violation(name: string): SeeViolationChain;
|
|
379
528
|
/**
|
|
380
529
|
* Mark an exception as expected control flow — documents the expectation and
|
|
381
|
-
* reports nothing.
|
|
530
|
+
* reports nothing. Say why with `.because()` (reason should start with
|
|
531
|
+
* "because"); attach optional debug context with `.extras()`.
|
|
532
|
+
*
|
|
533
|
+
* ```ts
|
|
534
|
+
* see.ControlFlowException(e).because("because the metric may not exist yet");
|
|
535
|
+
* ```
|
|
382
536
|
*/
|
|
383
|
-
ControlFlowException(err: unknown
|
|
537
|
+
ControlFlowException(err: unknown): SeeControlFlowChain;
|
|
384
538
|
}
|
|
385
539
|
/**
|
|
386
540
|
* Structured error reporter — the whole grammar hangs off this one import.
|
|
@@ -389,4 +543,4 @@ interface SeeApi {
|
|
|
389
543
|
*/
|
|
390
544
|
declare const see: SeeApi;
|
|
391
545
|
|
|
392
|
-
export { ANON_ID_COOKIE, type BootstrapHtmlOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type I18nForRequest, type LabelFile, type SeeApi, type SeeChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, type Violation, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
|
|
546
|
+
export { ANON_ID_COOKIE, type BootstrapHtmlOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type ExpsBlob, FLAG_REASONS, type FetchLabelsOptions, type FlagDetail, type FlagReason, type FlagsBlob, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type GetConfigOptions, type I18nForRequest, type LabelFile, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, type Violation, _murmur3ForTests, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
|