@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.
@@ -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) so `.message()` can be a builder method without
14
- * colliding with `Error.prototype.message`.
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
- interface SeeViolationChain extends SeeChain {
91
- /** Free-form detail. Variable data goes here (or extras), never in the name. */
92
- message(msg: string): SeeViolationChain;
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
- getFlag(name: string, user: User): boolean;
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
- getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
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 `.message()` or `.extras()`.
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").causes_the("search results").to("be trimmed");
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. The reason must start with "because".
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, because: string): void;
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 };