@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.
@@ -104,6 +104,14 @@ interface SeeControlFlowChain {
104
104
  }
105
105
 
106
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;
107
115
  interface User {
108
116
  user_id?: string;
109
117
  anonymous_id?: string;
@@ -114,6 +122,30 @@ interface ExperimentResult<P> {
114
122
  group: string;
115
123
  params: P;
116
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
+ }
117
149
  interface GateRule {
118
150
  attr: string;
119
151
  op: "eq" | "neq" | "in" | "not_in" | "gt" | "gte" | "lt" | "lte" | "contains" | "regex";
@@ -126,10 +158,47 @@ interface Gate {
126
158
  enabled: 0 | 1 | boolean;
127
159
  killswitch?: 0 | 1 | boolean;
128
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
+ /** Attribute to bucket on (e.g. company_id); defaults to user_id/anonymous_id. */
174
+ bucketBy?: string | null;
175
+ }
176
+ /** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
177
+ interface StickyEntry {
178
+ g: string;
179
+ s: string;
180
+ }
181
+ /**
182
+ * Pluggable sticky-bucketing store for the server (doc 20 §2). Keyed by the
183
+ * bucketing unit; the value is that unit's per-experiment assignments. Absent
184
+ * from {@link FlagsClientOptions} ⇒ today's deterministic behaviour. Use
185
+ * {@link createInMemoryStickyStore} or a cookie-bridge built from request
186
+ * cookies.
187
+ */
188
+ interface StickyBucketStore {
189
+ get(unit: string): Record<string, StickyEntry> | undefined;
190
+ set(unit: string, exp: string, entry: StickyEntry): void;
191
+ }
192
+ /** A process-local sticky store (Map-backed). Handy for tests + single-process servers. */
193
+ declare function createInMemoryStickyStore(seed?: Record<string, Record<string, StickyEntry>>): StickyBucketStore;
194
+ interface Universe {
195
+ holdout_range: [number, number] | null;
196
+ }
129
197
  interface Killswitch {
130
198
  killed: 0 | 1 | boolean;
131
199
  switches?: Record<string, 0 | 1 | boolean>;
132
200
  }
201
+ /** Body of `GET /sdk/flags` — the snapshot's `flags` field. See {@link FlagsClient.fromSnapshot}. */
133
202
  interface FlagsBlob {
134
203
  version: string;
135
204
  plan: string;
@@ -139,6 +208,12 @@ interface FlagsBlob {
139
208
  }>;
140
209
  killswitches: Record<string, Killswitch>;
141
210
  }
211
+ /** Body of `GET /sdk/experiments` — the snapshot's `experiments` field. See {@link FlagsClient.fromSnapshot}. */
212
+ interface ExpsBlob {
213
+ version: string;
214
+ universes: Record<string, Universe>;
215
+ experiments: Record<string, Experiment>;
216
+ }
142
217
  interface BootstrapPayload {
143
218
  flags: Record<string, boolean>;
144
219
  configs: Record<string, unknown>;
@@ -168,11 +243,35 @@ interface FlagsClientOptions {
168
243
  disableTelemetry?: boolean;
169
244
  /** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
170
245
  telemetryUrl?: string;
246
+ /**
247
+ * Attribute names usable for targeting but never persisted in analytics
248
+ * (LD/Statsig `privateAttributes`). The server evaluates locally so private
249
+ * attrs never leave for evaluation at all; the only egress is `/collect`, and
250
+ * the listed keys are stripped from every outbound `track()` payload.
251
+ */
252
+ privateAttributes?: string[];
253
+ /**
254
+ * Sticky-bucketing store (doc 20 §2). When provided, `getExperiment` locks a
255
+ * unit to its first-assigned variant — changing allocation % or weights won't
256
+ * re-bucket enrolled units (changing the experiment salt is the reshuffle
257
+ * lever). Absent ⇒ deterministic (fully backward compatible). Built-ins:
258
+ * {@link createInMemoryStickyStore}, or a cookie-bridge over `__se_sticky`.
259
+ */
260
+ stickyStore?: StickyBucketStore;
261
+ /**
262
+ * Test mode — no network at all. init()/initOnce() are no-ops (never fetch),
263
+ * track() is a no-op, telemetry is forced off, and the client starts
264
+ * "initialized" with an empty blob. Prefer the {@link FlagsClient.forTesting}
265
+ * factory over passing this directly.
266
+ */
267
+ testMode?: boolean;
171
268
  }
172
269
  declare class FlagsClient {
173
270
  private readonly apiKey;
174
271
  private readonly baseUrl;
175
272
  private readonly env;
273
+ private readonly privateAttributes;
274
+ private readonly stickyStore;
176
275
  private readonly telemetry;
177
276
  private readonly seeLimiter;
178
277
  private flagsBlob;
@@ -182,18 +281,102 @@ declare class FlagsClient {
182
281
  private pollInterval;
183
282
  private timer;
184
283
  private initialized;
284
+ private readonly testMode;
285
+ private readonly flagOverrides;
286
+ private readonly configOverrides;
287
+ private readonly experimentOverrides;
288
+ private readonly changeListeners;
185
289
  constructor(opts: FlagsClientOptions);
290
+ /**
291
+ * Build a no-network, immediately-usable client for tests (Statsig-style).
292
+ * init()/initOnce() are no-ops (never fetch), track() is a no-op, telemetry
293
+ * is disabled, and the client is already "initialized" — seed every entity
294
+ * with overrideFlag/overrideConfig/overrideExperiment. No SDK key required.
295
+ *
296
+ * ```ts
297
+ * const client = FlagsClient.forTesting();
298
+ * client.overrideFlag("new_checkout", true);
299
+ * client.getFlag("new_checkout", { user_id: "u1" }); // true
300
+ * ```
301
+ */
302
+ static forTesting(opts?: Partial<FlagsClientOptions>): FlagsClient;
303
+ /**
304
+ * Build a fully OFFLINE client from a pre-captured snapshot — no network ever.
305
+ * Reuses the test-mode plumbing (init()/initOnce()/track() are no-ops,
306
+ * telemetry off) but, unlike forTesting(), seeds the REAL flags + experiments
307
+ * blobs so evaluations run the canonical eval against the snapshot. Local
308
+ * overrides still apply on top.
309
+ *
310
+ * Snapshot shape mirrors the wire bodies:
311
+ * `{ flags: <GET /sdk/flags body>, experiments: <GET /sdk/experiments body> }`.
312
+ */
313
+ static fromSnapshot(snapshot: {
314
+ flags: FlagsBlob;
315
+ experiments: ExpsBlob;
316
+ }): FlagsClient;
317
+ /**
318
+ * Build a fully OFFLINE client from a snapshot JSON file on disk (Node only —
319
+ * not available in the browser entrypoint). The file must contain
320
+ * `{ "flags": <GET /sdk/flags body>, "experiments": <GET /sdk/experiments body> }`.
321
+ * See {@link FlagsClient.fromSnapshot}.
322
+ */
323
+ static fromFile(path: string): FlagsClient;
186
324
  init(): Promise<void>;
187
325
  initOnce(): Promise<void>;
326
+ /** Force `getFlag(name, …)` to return `value`, ignoring the fetched gate. */
327
+ overrideFlag(name: string, value: boolean): void;
328
+ /** Force `getConfig(name)` to return `value`, ignoring the fetched config. */
329
+ overrideConfig(name: string, value: unknown): void;
330
+ /**
331
+ * Force `getExperiment(name, …)` to return `{ inExperiment: true, group, params }`,
332
+ * ignoring allocation, holdouts, and targeting.
333
+ */
334
+ overrideExperiment(name: string, group: string, params: Record<string, unknown>): void;
335
+ /** Remove every programmatic override set via the override* methods. */
336
+ clearOverrides(): void;
188
337
  destroy(): void;
338
+ /**
339
+ * Subscribe to data-change notifications. The listener fires after a
340
+ * background poll fetch returns NEW data (200, not 304) — i.e. after the
341
+ * cached blob is updated. Never fires in testMode/offline (no polling).
342
+ * Returns an unsubscribe function.
343
+ */
344
+ onChange(listener: () => void): () => void;
345
+ private notifyChange;
189
346
  private startPoll;
190
347
  private fetchAll;
191
348
  private fetchFlags;
192
349
  private fetchExps;
193
- getFlag(name: string, user: User): boolean;
350
+ /**
351
+ * Evaluate a gate and report WHY (LaunchDarkly variationDetail parity). The
352
+ * reason is computed entirely at this boundary — the canonical eval
353
+ * (evalGateInternal) is untouched. A local override short-circuits BEFORE
354
+ * telemetry, exactly like getFlag's override path; otherwise exactly one
355
+ * "gate" beacon is emitted.
356
+ */
357
+ getFlagDetail(name: string, user: User): FlagDetail;
358
+ /**
359
+ * Read a feature gate. Returns `defaultValue` ONLY when the gate cannot be
360
+ * evaluated (client not initialized or flag not found) — never for a gate that
361
+ * legitimately evaluates to false. Plain `getFlag(name, user)` keeps returning
362
+ * false for a missing flag.
363
+ */
364
+ getFlag(name: string, user: User, defaultValue?: boolean): boolean;
194
365
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
366
+ getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
195
367
  getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
368
+ /** Drop caller-marked private attributes from an outbound props bag. */
369
+ private stripPrivate;
196
370
  track(userId: string, eventName: string, props?: Record<string, unknown>): void;
371
+ /**
372
+ * Emit an exposure event for an experiment at the server-side decision point
373
+ * (parity with the browser's auto-exposure). The server is stateless and
374
+ * never auto-logs, so call this when you actually present the treatment.
375
+ * Re-evaluates the experiment for `user` (a bare `user_id` string is wrapped
376
+ * as `{ user_id }`); if the user is enrolled, POSTs a single exposure to
377
+ * `/collect`. No-op in test mode or when the user isn't enrolled.
378
+ */
379
+ logExposure(user: string | User, name: string): void;
197
380
  /**
198
381
  * Report a structured error into the errors primitive. Fire-and-forget —
199
382
  * never blocks or throws into the request path. Spam-guarded by a 30s
@@ -283,6 +466,12 @@ interface ShipeasyServerConfig {
283
466
  * that evaluate many flags per request. See {@link FlagsClientOptions.disableTelemetry}.
284
467
  */
285
468
  disableTelemetry?: boolean;
469
+ /**
470
+ * Attribute names usable for targeting but never persisted in analytics
471
+ * (LD/Statsig `privateAttributes`). Stripped from every outbound `track()`
472
+ * payload. See {@link FlagsClientOptions.privateAttributes}.
473
+ */
474
+ privateAttributes?: string[];
286
475
  }
287
476
  interface ShipeasyServerHandle {
288
477
  flags: Record<string, boolean>;
@@ -338,8 +527,10 @@ declare const flags: {
338
527
  initOnce(): Promise<void>;
339
528
  /** Stop background timers. Safe to call repeatedly. */
340
529
  destroy(): void;
341
- get(name: string, user: User): boolean;
342
- getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
530
+ get(name: string, user: User, defaultValue?: boolean): boolean;
531
+ /** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
532
+ getDetail(name: string, user: User): FlagDetail;
533
+ getConfig<T = unknown>(name: string, decodeOrOpts?: ((raw: unknown) => T) | GetConfigOptions<T>): T | undefined;
343
534
  getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
344
535
  /**
345
536
  * Read a killswitch. Without `switchKey`, returns true when the whole
@@ -348,6 +539,9 @@ declare const flags: {
348
539
  */
349
540
  ks(name: string, switchKey?: string): boolean;
350
541
  track(userId: string, eventName: string, props?: Record<string, unknown>): void;
542
+ /** Emit an exposure for an enrolled experiment at the decision point. See
543
+ * {@link FlagsClient.logExposure}. No-op before configure(). */
544
+ logExposure(user: string | User, name: string): void;
351
545
  /**
352
546
  * Evaluate all flags / configs / experiments for a user against the locally
353
547
  * cached blob. Pass the request URL to apply ?se_ks_* / ?se_cf_* / ?se_exp_*
@@ -406,4 +600,4 @@ interface SeeApi {
406
600
  */
407
601
  declare const see: SeeApi;
408
602
 
409
- 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 SeeControlFlowChain, 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 };
603
+ 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 StickyBucketStore, type StickyEntry, type User, type Violation, _murmur3ForTests, _resetShipeasyServerForTests, configureShipeasyServer, createInMemoryStickyStore, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
@@ -104,6 +104,14 @@ interface SeeControlFlowChain {
104
104
  }
105
105
 
106
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;
107
115
  interface User {
108
116
  user_id?: string;
109
117
  anonymous_id?: string;
@@ -114,6 +122,30 @@ interface ExperimentResult<P> {
114
122
  group: string;
115
123
  params: P;
116
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
+ }
117
149
  interface GateRule {
118
150
  attr: string;
119
151
  op: "eq" | "neq" | "in" | "not_in" | "gt" | "gte" | "lt" | "lte" | "contains" | "regex";
@@ -126,10 +158,47 @@ interface Gate {
126
158
  enabled: 0 | 1 | boolean;
127
159
  killswitch?: 0 | 1 | boolean;
128
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
+ /** Attribute to bucket on (e.g. company_id); defaults to user_id/anonymous_id. */
174
+ bucketBy?: string | null;
175
+ }
176
+ /** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
177
+ interface StickyEntry {
178
+ g: string;
179
+ s: string;
180
+ }
181
+ /**
182
+ * Pluggable sticky-bucketing store for the server (doc 20 §2). Keyed by the
183
+ * bucketing unit; the value is that unit's per-experiment assignments. Absent
184
+ * from {@link FlagsClientOptions} ⇒ today's deterministic behaviour. Use
185
+ * {@link createInMemoryStickyStore} or a cookie-bridge built from request
186
+ * cookies.
187
+ */
188
+ interface StickyBucketStore {
189
+ get(unit: string): Record<string, StickyEntry> | undefined;
190
+ set(unit: string, exp: string, entry: StickyEntry): void;
191
+ }
192
+ /** A process-local sticky store (Map-backed). Handy for tests + single-process servers. */
193
+ declare function createInMemoryStickyStore(seed?: Record<string, Record<string, StickyEntry>>): StickyBucketStore;
194
+ interface Universe {
195
+ holdout_range: [number, number] | null;
196
+ }
129
197
  interface Killswitch {
130
198
  killed: 0 | 1 | boolean;
131
199
  switches?: Record<string, 0 | 1 | boolean>;
132
200
  }
201
+ /** Body of `GET /sdk/flags` — the snapshot's `flags` field. See {@link FlagsClient.fromSnapshot}. */
133
202
  interface FlagsBlob {
134
203
  version: string;
135
204
  plan: string;
@@ -139,6 +208,12 @@ interface FlagsBlob {
139
208
  }>;
140
209
  killswitches: Record<string, Killswitch>;
141
210
  }
211
+ /** Body of `GET /sdk/experiments` — the snapshot's `experiments` field. See {@link FlagsClient.fromSnapshot}. */
212
+ interface ExpsBlob {
213
+ version: string;
214
+ universes: Record<string, Universe>;
215
+ experiments: Record<string, Experiment>;
216
+ }
142
217
  interface BootstrapPayload {
143
218
  flags: Record<string, boolean>;
144
219
  configs: Record<string, unknown>;
@@ -168,11 +243,35 @@ interface FlagsClientOptions {
168
243
  disableTelemetry?: boolean;
169
244
  /** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
170
245
  telemetryUrl?: string;
246
+ /**
247
+ * Attribute names usable for targeting but never persisted in analytics
248
+ * (LD/Statsig `privateAttributes`). The server evaluates locally so private
249
+ * attrs never leave for evaluation at all; the only egress is `/collect`, and
250
+ * the listed keys are stripped from every outbound `track()` payload.
251
+ */
252
+ privateAttributes?: string[];
253
+ /**
254
+ * Sticky-bucketing store (doc 20 §2). When provided, `getExperiment` locks a
255
+ * unit to its first-assigned variant — changing allocation % or weights won't
256
+ * re-bucket enrolled units (changing the experiment salt is the reshuffle
257
+ * lever). Absent ⇒ deterministic (fully backward compatible). Built-ins:
258
+ * {@link createInMemoryStickyStore}, or a cookie-bridge over `__se_sticky`.
259
+ */
260
+ stickyStore?: StickyBucketStore;
261
+ /**
262
+ * Test mode — no network at all. init()/initOnce() are no-ops (never fetch),
263
+ * track() is a no-op, telemetry is forced off, and the client starts
264
+ * "initialized" with an empty blob. Prefer the {@link FlagsClient.forTesting}
265
+ * factory over passing this directly.
266
+ */
267
+ testMode?: boolean;
171
268
  }
172
269
  declare class FlagsClient {
173
270
  private readonly apiKey;
174
271
  private readonly baseUrl;
175
272
  private readonly env;
273
+ private readonly privateAttributes;
274
+ private readonly stickyStore;
176
275
  private readonly telemetry;
177
276
  private readonly seeLimiter;
178
277
  private flagsBlob;
@@ -182,18 +281,102 @@ declare class FlagsClient {
182
281
  private pollInterval;
183
282
  private timer;
184
283
  private initialized;
284
+ private readonly testMode;
285
+ private readonly flagOverrides;
286
+ private readonly configOverrides;
287
+ private readonly experimentOverrides;
288
+ private readonly changeListeners;
185
289
  constructor(opts: FlagsClientOptions);
290
+ /**
291
+ * Build a no-network, immediately-usable client for tests (Statsig-style).
292
+ * init()/initOnce() are no-ops (never fetch), track() is a no-op, telemetry
293
+ * is disabled, and the client is already "initialized" — seed every entity
294
+ * with overrideFlag/overrideConfig/overrideExperiment. No SDK key required.
295
+ *
296
+ * ```ts
297
+ * const client = FlagsClient.forTesting();
298
+ * client.overrideFlag("new_checkout", true);
299
+ * client.getFlag("new_checkout", { user_id: "u1" }); // true
300
+ * ```
301
+ */
302
+ static forTesting(opts?: Partial<FlagsClientOptions>): FlagsClient;
303
+ /**
304
+ * Build a fully OFFLINE client from a pre-captured snapshot — no network ever.
305
+ * Reuses the test-mode plumbing (init()/initOnce()/track() are no-ops,
306
+ * telemetry off) but, unlike forTesting(), seeds the REAL flags + experiments
307
+ * blobs so evaluations run the canonical eval against the snapshot. Local
308
+ * overrides still apply on top.
309
+ *
310
+ * Snapshot shape mirrors the wire bodies:
311
+ * `{ flags: <GET /sdk/flags body>, experiments: <GET /sdk/experiments body> }`.
312
+ */
313
+ static fromSnapshot(snapshot: {
314
+ flags: FlagsBlob;
315
+ experiments: ExpsBlob;
316
+ }): FlagsClient;
317
+ /**
318
+ * Build a fully OFFLINE client from a snapshot JSON file on disk (Node only —
319
+ * not available in the browser entrypoint). The file must contain
320
+ * `{ "flags": <GET /sdk/flags body>, "experiments": <GET /sdk/experiments body> }`.
321
+ * See {@link FlagsClient.fromSnapshot}.
322
+ */
323
+ static fromFile(path: string): FlagsClient;
186
324
  init(): Promise<void>;
187
325
  initOnce(): Promise<void>;
326
+ /** Force `getFlag(name, …)` to return `value`, ignoring the fetched gate. */
327
+ overrideFlag(name: string, value: boolean): void;
328
+ /** Force `getConfig(name)` to return `value`, ignoring the fetched config. */
329
+ overrideConfig(name: string, value: unknown): void;
330
+ /**
331
+ * Force `getExperiment(name, …)` to return `{ inExperiment: true, group, params }`,
332
+ * ignoring allocation, holdouts, and targeting.
333
+ */
334
+ overrideExperiment(name: string, group: string, params: Record<string, unknown>): void;
335
+ /** Remove every programmatic override set via the override* methods. */
336
+ clearOverrides(): void;
188
337
  destroy(): void;
338
+ /**
339
+ * Subscribe to data-change notifications. The listener fires after a
340
+ * background poll fetch returns NEW data (200, not 304) — i.e. after the
341
+ * cached blob is updated. Never fires in testMode/offline (no polling).
342
+ * Returns an unsubscribe function.
343
+ */
344
+ onChange(listener: () => void): () => void;
345
+ private notifyChange;
189
346
  private startPoll;
190
347
  private fetchAll;
191
348
  private fetchFlags;
192
349
  private fetchExps;
193
- getFlag(name: string, user: User): boolean;
350
+ /**
351
+ * Evaluate a gate and report WHY (LaunchDarkly variationDetail parity). The
352
+ * reason is computed entirely at this boundary — the canonical eval
353
+ * (evalGateInternal) is untouched. A local override short-circuits BEFORE
354
+ * telemetry, exactly like getFlag's override path; otherwise exactly one
355
+ * "gate" beacon is emitted.
356
+ */
357
+ getFlagDetail(name: string, user: User): FlagDetail;
358
+ /**
359
+ * Read a feature gate. Returns `defaultValue` ONLY when the gate cannot be
360
+ * evaluated (client not initialized or flag not found) — never for a gate that
361
+ * legitimately evaluates to false. Plain `getFlag(name, user)` keeps returning
362
+ * false for a missing flag.
363
+ */
364
+ getFlag(name: string, user: User, defaultValue?: boolean): boolean;
194
365
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
366
+ getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
195
367
  getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
368
+ /** Drop caller-marked private attributes from an outbound props bag. */
369
+ private stripPrivate;
196
370
  track(userId: string, eventName: string, props?: Record<string, unknown>): void;
371
+ /**
372
+ * Emit an exposure event for an experiment at the server-side decision point
373
+ * (parity with the browser's auto-exposure). The server is stateless and
374
+ * never auto-logs, so call this when you actually present the treatment.
375
+ * Re-evaluates the experiment for `user` (a bare `user_id` string is wrapped
376
+ * as `{ user_id }`); if the user is enrolled, POSTs a single exposure to
377
+ * `/collect`. No-op in test mode or when the user isn't enrolled.
378
+ */
379
+ logExposure(user: string | User, name: string): void;
197
380
  /**
198
381
  * Report a structured error into the errors primitive. Fire-and-forget —
199
382
  * never blocks or throws into the request path. Spam-guarded by a 30s
@@ -283,6 +466,12 @@ interface ShipeasyServerConfig {
283
466
  * that evaluate many flags per request. See {@link FlagsClientOptions.disableTelemetry}.
284
467
  */
285
468
  disableTelemetry?: boolean;
469
+ /**
470
+ * Attribute names usable for targeting but never persisted in analytics
471
+ * (LD/Statsig `privateAttributes`). Stripped from every outbound `track()`
472
+ * payload. See {@link FlagsClientOptions.privateAttributes}.
473
+ */
474
+ privateAttributes?: string[];
286
475
  }
287
476
  interface ShipeasyServerHandle {
288
477
  flags: Record<string, boolean>;
@@ -338,8 +527,10 @@ declare const flags: {
338
527
  initOnce(): Promise<void>;
339
528
  /** Stop background timers. Safe to call repeatedly. */
340
529
  destroy(): void;
341
- get(name: string, user: User): boolean;
342
- getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
530
+ get(name: string, user: User, defaultValue?: boolean): boolean;
531
+ /** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
532
+ getDetail(name: string, user: User): FlagDetail;
533
+ getConfig<T = unknown>(name: string, decodeOrOpts?: ((raw: unknown) => T) | GetConfigOptions<T>): T | undefined;
343
534
  getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
344
535
  /**
345
536
  * Read a killswitch. Without `switchKey`, returns true when the whole
@@ -348,6 +539,9 @@ declare const flags: {
348
539
  */
349
540
  ks(name: string, switchKey?: string): boolean;
350
541
  track(userId: string, eventName: string, props?: Record<string, unknown>): void;
542
+ /** Emit an exposure for an enrolled experiment at the decision point. See
543
+ * {@link FlagsClient.logExposure}. No-op before configure(). */
544
+ logExposure(user: string | User, name: string): void;
351
545
  /**
352
546
  * Evaluate all flags / configs / experiments for a user against the locally
353
547
  * cached blob. Pass the request URL to apply ?se_ks_* / ?se_cf_* / ?se_exp_*
@@ -406,4 +600,4 @@ interface SeeApi {
406
600
  */
407
601
  declare const see: SeeApi;
408
602
 
409
- 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 SeeControlFlowChain, 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 };
603
+ 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 StickyBucketStore, type StickyEntry, type User, type Violation, _murmur3ForTests, _resetShipeasyServerForTests, configureShipeasyServer, createInMemoryStickyStore, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };