@shipeasy/sdk 6.3.1 → 7.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 +1 -4
- package/dist/client/index.d.mts +166 -27
- package/dist/client/index.d.ts +166 -27
- package/dist/client/index.js +268 -132
- package/dist/client/index.mjs +268 -132
- package/dist/openfeature-server/index.d.mts +144 -14
- package/dist/openfeature-server/index.d.ts +144 -14
- package/dist/openfeature-server/index.js +65 -33
- package/dist/openfeature-server/index.mjs +65 -33
- package/dist/openfeature-web/index.d.mts +110 -30
- package/dist/openfeature-web/index.d.ts +110 -30
- package/dist/server/index.d.mts +181 -28
- package/dist/server/index.d.ts +181 -28
- package/dist/server/index.js +337 -159
- package/dist/server/index.mjs +337 -159
- package/docs/skill/SKILL.md +20 -10
- package/package.json +3 -1
|
@@ -4,43 +4,12 @@ import { ErrorCode } from "@openfeature/server-sdk";
|
|
|
4
4
|
// src/server/index.ts
|
|
5
5
|
import { AsyncLocalStorage } from "async_hooks";
|
|
6
6
|
|
|
7
|
-
// src/logger.ts
|
|
8
|
-
var RANK = {
|
|
9
|
-
silent: 0,
|
|
10
|
-
error: 1,
|
|
11
|
-
warn: 2,
|
|
12
|
-
info: 3,
|
|
13
|
-
debug: 4
|
|
14
|
-
};
|
|
15
|
-
var currentLevel = "warn";
|
|
16
|
-
function write(method, args) {
|
|
17
|
-
try {
|
|
18
|
-
const c = globalThis.console;
|
|
19
|
-
if (!c) return;
|
|
20
|
-
const fn = c[method] ?? c.log;
|
|
21
|
-
if (typeof fn === "function") fn.call(c, ...args);
|
|
22
|
-
} catch {
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
var logger = {
|
|
26
|
-
error(...args) {
|
|
27
|
-
if (RANK[currentLevel] >= RANK.error) write("error", args);
|
|
28
|
-
},
|
|
29
|
-
warn(...args) {
|
|
30
|
-
if (RANK[currentLevel] >= RANK.warn) write("warn", args);
|
|
31
|
-
},
|
|
32
|
-
info(...args) {
|
|
33
|
-
if (RANK[currentLevel] >= RANK.info) write("info", args);
|
|
34
|
-
},
|
|
35
|
-
debug(...args) {
|
|
36
|
-
if (RANK[currentLevel] >= RANK.debug) write("debug", args);
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
|
|
40
7
|
// src/see/core.ts
|
|
41
8
|
var SEE_MAX_SUBJECT = 200;
|
|
42
9
|
var SEE_MAX_EXTRA_VALUE = 200;
|
|
43
10
|
var SEE_MAX_EXTRA_KEYS = 20;
|
|
11
|
+
var SEE_DEDUP_WINDOW_MS = 3e4;
|
|
12
|
+
var SEE_MAX_PER_SESSION = 25;
|
|
44
13
|
function causesThe(subject) {
|
|
45
14
|
return {
|
|
46
15
|
to(outcome) {
|
|
@@ -151,6 +120,69 @@ function startControlFlowChain(err) {
|
|
|
151
120
|
}
|
|
152
121
|
};
|
|
153
122
|
}
|
|
123
|
+
function topStackLine(stack) {
|
|
124
|
+
if (!stack) return "";
|
|
125
|
+
for (const line of stack.split("\n")) {
|
|
126
|
+
if (/^\s*at |@|:\d+:\d+/.test(line)) return line.trim().slice(0, 200);
|
|
127
|
+
}
|
|
128
|
+
return "";
|
|
129
|
+
}
|
|
130
|
+
var SeeLimiter = class {
|
|
131
|
+
constructor(maxPerSession = SEE_MAX_PER_SESSION, dedupWindowMs = SEE_DEDUP_WINDOW_MS) {
|
|
132
|
+
this.maxPerSession = maxPerSession;
|
|
133
|
+
this.dedupWindowMs = dedupWindowMs;
|
|
134
|
+
}
|
|
135
|
+
maxPerSession;
|
|
136
|
+
dedupWindowMs;
|
|
137
|
+
lastSent = /* @__PURE__ */ new Map();
|
|
138
|
+
sent = 0;
|
|
139
|
+
shouldSend(ev) {
|
|
140
|
+
if (this.sent >= this.maxPerSession) return false;
|
|
141
|
+
const key = `${ev.kind}|${ev.error_type}|${ev.message.slice(0, 200)}|${topStackLine(ev.stack)}`;
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
const prev = this.lastSent.get(key);
|
|
144
|
+
if (prev !== void 0 && now - prev < this.dedupWindowMs) return false;
|
|
145
|
+
this.lastSent.set(key, now);
|
|
146
|
+
this.sent += 1;
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// src/internal-report.ts
|
|
152
|
+
var limiter = new SeeLimiter();
|
|
153
|
+
|
|
154
|
+
// src/logger.ts
|
|
155
|
+
var RANK = {
|
|
156
|
+
silent: 0,
|
|
157
|
+
error: 1,
|
|
158
|
+
warn: 2,
|
|
159
|
+
info: 3,
|
|
160
|
+
debug: 4
|
|
161
|
+
};
|
|
162
|
+
var currentLevel = "warn";
|
|
163
|
+
function write(method, args) {
|
|
164
|
+
try {
|
|
165
|
+
const c = globalThis.console;
|
|
166
|
+
if (!c) return;
|
|
167
|
+
const fn = c[method] ?? c.log;
|
|
168
|
+
if (typeof fn === "function") fn.call(c, ...args);
|
|
169
|
+
} catch {
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
var logger = {
|
|
173
|
+
error(...args) {
|
|
174
|
+
if (RANK[currentLevel] >= RANK.error) write("error", args);
|
|
175
|
+
},
|
|
176
|
+
warn(...args) {
|
|
177
|
+
if (RANK[currentLevel] >= RANK.warn) write("warn", args);
|
|
178
|
+
},
|
|
179
|
+
info(...args) {
|
|
180
|
+
if (RANK[currentLevel] >= RANK.info) write("info", args);
|
|
181
|
+
},
|
|
182
|
+
debug(...args) {
|
|
183
|
+
if (RANK[currentLevel] >= RANK.debug) write("debug", args);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
154
186
|
|
|
155
187
|
// src/server/index.ts
|
|
156
188
|
var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
|
|
@@ -25,25 +25,6 @@ interface User {
|
|
|
25
25
|
user_id?: string;
|
|
26
26
|
[attr: string]: unknown;
|
|
27
27
|
}
|
|
28
|
-
interface ExperimentResult<P> {
|
|
29
|
-
inExperiment: boolean;
|
|
30
|
-
group: string;
|
|
31
|
-
params: P;
|
|
32
|
-
}
|
|
33
|
-
/** Options object form of `getExperiment` — the legacy `decode`/`variants`
|
|
34
|
-
* positional args plus per-call exposure control. */
|
|
35
|
-
interface GetExperimentOptions<P> {
|
|
36
|
-
/** Decode the raw stored params into the typed shape callers want. */
|
|
37
|
-
decode?: (raw: unknown) => P;
|
|
38
|
-
/** Variant-specific param overrides merged on top of group params. */
|
|
39
|
-
variants?: Record<string, Partial<P>>;
|
|
40
|
-
/**
|
|
41
|
-
* Override automatic exposure logging for this read. Defaults to the client's
|
|
42
|
-
* setting (`disableAutoExposure` flips it). `false` reads the variant without
|
|
43
|
-
* logging an exposure — pair with `logExposure(name)` at render time.
|
|
44
|
-
*/
|
|
45
|
-
logExposure?: boolean;
|
|
46
|
-
}
|
|
47
28
|
/**
|
|
48
29
|
* Why a flag evaluated the way it did (LaunchDarkly variationDetail parity).
|
|
49
30
|
* Computed at the client boundary:
|
|
@@ -75,11 +56,19 @@ interface EvalExpResult {
|
|
|
75
56
|
inExperiment: boolean;
|
|
76
57
|
group: string;
|
|
77
58
|
params: Record<string, unknown>;
|
|
59
|
+
/** The universe this experiment belongs to — lets `universe(name).assign()`
|
|
60
|
+
* find the enrolled experiment within a universe. */
|
|
61
|
+
universe?: string;
|
|
78
62
|
}
|
|
79
63
|
interface EvalResponse {
|
|
80
64
|
flags: Record<string, boolean>;
|
|
81
65
|
configs: Record<string, unknown>;
|
|
82
66
|
experiments: Record<string, EvalExpResult>;
|
|
67
|
+
/** Per-universe param defaults (name → default map) so `universe(name).get()`
|
|
68
|
+
* resolves to the universe default even when the unit is not enrolled. */
|
|
69
|
+
universes?: Record<string, {
|
|
70
|
+
defaults: Record<string, unknown>;
|
|
71
|
+
}>;
|
|
83
72
|
/**
|
|
84
73
|
* Killswitch state, flattened by the server. A boolean means the killswitch
|
|
85
74
|
* is whole-killed; an object means it's not whole-killed and carries per-
|
|
@@ -93,6 +82,23 @@ interface EvalResponse {
|
|
|
93
82
|
*/
|
|
94
83
|
sticky?: Record<string, StickyCookieEntry>;
|
|
95
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* The result of `universe(name).assign()` — the visitor's standing in a universe.
|
|
87
|
+
* A universe is a mutual-exclusion pool, so the visitor lands in **at most one**
|
|
88
|
+
* experiment. Never throws: an un-enrolled visitor still resolves `get()` to the
|
|
89
|
+
* universe defaults (or your fallback). `assign()` auto-logs one exposure when
|
|
90
|
+
* enrolled (subject to `disableAutoExposure`).
|
|
91
|
+
*/
|
|
92
|
+
interface Assignment {
|
|
93
|
+
/** The experiment the visitor landed in, or `null` when not enrolled. */
|
|
94
|
+
readonly name: string | null;
|
|
95
|
+
/** The assigned variant/group name, or `null` when not enrolled. */
|
|
96
|
+
readonly group: string | null;
|
|
97
|
+
/** True iff the visitor is enrolled in an experiment in this universe. */
|
|
98
|
+
readonly enrolled: boolean;
|
|
99
|
+
/** Read a resolved param: variant override ?? universe default ?? `fallback`. */
|
|
100
|
+
get<T = unknown>(field: string, fallback?: T): T | undefined;
|
|
101
|
+
}
|
|
96
102
|
/** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
|
|
97
103
|
interface StickyCookieEntry {
|
|
98
104
|
g: string;
|
|
@@ -104,6 +110,33 @@ interface AutoCollectGroups {
|
|
|
104
110
|
engagement: boolean;
|
|
105
111
|
}
|
|
106
112
|
type EngineEnv = "dev" | "staging" | "prod";
|
|
113
|
+
/**
|
|
114
|
+
* A pluggable persistence backend for the anonymous id. Primarily for non-DOM
|
|
115
|
+
* runtimes (React Native / Expo) where there is no cookie or `localStorage`, so
|
|
116
|
+
* the anon id would otherwise regenerate every app launch and re-bucket the
|
|
117
|
+
* visitor. Point it at `@react-native-async-storage/async-storage` (or any
|
|
118
|
+
* store) and the SDK hydrates a stable id before the first `/sdk/evaluate`:
|
|
119
|
+
*
|
|
120
|
+
* ```ts
|
|
121
|
+
* import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
122
|
+
* configure({
|
|
123
|
+
* clientKey: "...",
|
|
124
|
+
* anonymousStore: {
|
|
125
|
+
* get: (k) => AsyncStorage.getItem(k),
|
|
126
|
+
* set: (k, v) => AsyncStorage.setItem(k, v),
|
|
127
|
+
* remove: (k) => AsyncStorage.removeItem(k),
|
|
128
|
+
* },
|
|
129
|
+
* });
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* Methods may be sync or async — the SDK awaits either. Any thrown error is
|
|
133
|
+
* swallowed (the in-memory id is used as a fallback).
|
|
134
|
+
*/
|
|
135
|
+
interface AnonymousStore {
|
|
136
|
+
get(key: string): string | null | Promise<string | null>;
|
|
137
|
+
set(key: string, value: string): void | Promise<void>;
|
|
138
|
+
remove(key: string): void | Promise<void>;
|
|
139
|
+
}
|
|
107
140
|
interface EngineOptions {
|
|
108
141
|
sdkKey: string;
|
|
109
142
|
baseUrl?: string;
|
|
@@ -125,10 +158,26 @@ interface EngineOptions {
|
|
|
125
158
|
/** Which published env to read values from. Defaults to "prod". */
|
|
126
159
|
env?: EngineEnv;
|
|
127
160
|
/**
|
|
128
|
-
*
|
|
161
|
+
* Master network switch — when `false`, the browser SDK makes NO outbound
|
|
162
|
+
* requests at all: identify()/init never call /sdk/evaluate (getters read code
|
|
163
|
+
* defaults / overrides), track() and exposure logging are no-ops, usage
|
|
164
|
+
* telemetry is off, and SDK-internal error self-monitoring is off.
|
|
165
|
+
*
|
|
166
|
+
* DEFAULT is environment-derived (see {@link isProductionEnv}): ON in
|
|
167
|
+
* production, OFF everywhere else. In the browser there is usually no native
|
|
168
|
+
* `NODE_ENV`, so production is taken from the `env` option above (default
|
|
169
|
+
* `"prod"`) — meaning it defaults ON. Pass `false` (or set `env: "dev"`) to
|
|
170
|
+
* keep a local build quiet.
|
|
171
|
+
*/
|
|
172
|
+
isNetworkEnabled?: boolean;
|
|
173
|
+
/**
|
|
174
|
+
* Per-evaluation usage telemetry ("tracking") — each getFlag/getConfig/
|
|
129
175
|
* getExperiment/getKillswitch call fires one fire-and-forget sendBeacon so
|
|
130
|
-
* usage is counted by Cloudflare's native per-path analytics.
|
|
131
|
-
*
|
|
176
|
+
* usage is counted by Cloudflare's native per-path analytics.
|
|
177
|
+
*
|
|
178
|
+
* DEFAULT is environment-derived: ON in production, OFF everywhere else (same
|
|
179
|
+
* inference as {@link isNetworkEnabled}). Pass `true` to force it off, `false`
|
|
180
|
+
* to force it on. Forced off whenever `isNetworkEnabled` is false.
|
|
132
181
|
*/
|
|
133
182
|
disableTelemetry?: boolean;
|
|
134
183
|
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
@@ -141,6 +190,14 @@ interface EngineOptions {
|
|
|
141
190
|
* `silent` < `error` < `warn` < `info` < `debug`. Defaults to `"warn"`.
|
|
142
191
|
*/
|
|
143
192
|
logLevel?: LogLevel;
|
|
193
|
+
/**
|
|
194
|
+
* Opt out of SDK-internal error self-monitoring. When one of the SDK's own
|
|
195
|
+
* last-resort guards catches an internal failure (an "on our end" bug), the
|
|
196
|
+
* SDK reports it to Shipeasy's own project so we can track SDK bugs across
|
|
197
|
+
* apps — never to your project. ON by default; forced off in test mode. Pass
|
|
198
|
+
* `true` to disable.
|
|
199
|
+
*/
|
|
200
|
+
disableInternalErrorReporting?: boolean;
|
|
144
201
|
/**
|
|
145
202
|
* Suppress automatic exposure logging in `getExperiment` (Statsig's
|
|
146
203
|
* `disableExposureLogging`). Default false — reading an enrolled variant
|
|
@@ -164,6 +221,14 @@ interface EngineOptions {
|
|
|
164
221
|
* lever. Pass `false` to opt out (pure deterministic eval).
|
|
165
222
|
*/
|
|
166
223
|
stickyBucketing?: boolean;
|
|
224
|
+
/**
|
|
225
|
+
* Pluggable persistence for the anonymous id (React Native / Expo, or any
|
|
226
|
+
* runtime without a cookie / `localStorage`). When set, the SDK hydrates the
|
|
227
|
+
* stored id before the first `/sdk/evaluate` so bucketing stays stable across
|
|
228
|
+
* app launches; on a fresh device it persists the freshly-minted id. See
|
|
229
|
+
* {@link AnonymousStore}.
|
|
230
|
+
*/
|
|
231
|
+
anonymousStore?: AnonymousStore;
|
|
167
232
|
/**
|
|
168
233
|
* Test mode — no network at all. identify()/init are no-ops (never call
|
|
169
234
|
* /sdk/evaluate), track() is a no-op, telemetry is forced off, and the client
|
|
@@ -184,6 +249,7 @@ declare class Engine {
|
|
|
184
249
|
private readonly env;
|
|
185
250
|
private evalResult;
|
|
186
251
|
private anonId;
|
|
252
|
+
private anonReady;
|
|
187
253
|
private userId;
|
|
188
254
|
private buffer;
|
|
189
255
|
private telemetry;
|
|
@@ -193,11 +259,20 @@ declare class Engine {
|
|
|
193
259
|
private overrideListenerInstalled;
|
|
194
260
|
private identifySeq;
|
|
195
261
|
private readonly testMode;
|
|
262
|
+
private readonly networkEnabled;
|
|
263
|
+
private readonly offline;
|
|
196
264
|
private readonly flagOverrides;
|
|
197
265
|
private readonly configOverrides;
|
|
198
266
|
private readonly experimentOverrides;
|
|
199
267
|
private onOverrideChange;
|
|
200
268
|
constructor(opts: EngineOptions);
|
|
269
|
+
/**
|
|
270
|
+
* Consult the caller-supplied {@link AnonymousStore}: adopt a persisted id so
|
|
271
|
+
* the visitor buckets identically across app launches, or persist the id just
|
|
272
|
+
* minted by getOrCreateAnonId() on a fresh device. Best-effort — any store
|
|
273
|
+
* failure leaves the in-memory id in place.
|
|
274
|
+
*/
|
|
275
|
+
private hydrateAnonFromStore;
|
|
201
276
|
/**
|
|
202
277
|
* Build a no-network, immediately-usable browser client for tests
|
|
203
278
|
* (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
|
|
@@ -251,16 +326,21 @@ declare class Engine {
|
|
|
251
326
|
getFlag(name: string, defaultValue?: boolean): boolean;
|
|
252
327
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
253
328
|
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
254
|
-
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
|
|
255
|
-
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, opts: GetExperimentOptions<P>): ExperimentResult<P>;
|
|
256
329
|
/**
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* the
|
|
260
|
-
*
|
|
261
|
-
* `
|
|
330
|
+
* Assign the visitor within a universe: `universe("checkout").assign()`. A
|
|
331
|
+
* universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
|
|
332
|
+
* the returned {@link Assignment} exposes `.group` / `.get(field, fallback)`
|
|
333
|
+
* and auto-logs one exposure when enrolled (unless `disableAutoExposure` or
|
|
334
|
+
* `assign({ logExposure: false })`). An un-enrolled visitor still resolves
|
|
335
|
+
* `get()` to the universe defaults. This is the sole experiment read path —
|
|
336
|
+
* there is no `getExperiment` (ask a universe, not an experiment).
|
|
262
337
|
*/
|
|
263
|
-
|
|
338
|
+
universe(name: string): {
|
|
339
|
+
assign(opts?: {
|
|
340
|
+
logExposure?: boolean;
|
|
341
|
+
}): Assignment;
|
|
342
|
+
};
|
|
343
|
+
private assignUniverse;
|
|
264
344
|
/**
|
|
265
345
|
* Subscribe to state changes — fires after identify() completes and on
|
|
266
346
|
* `se:override:change` events from the devtools overlay. Returns an
|
|
@@ -25,25 +25,6 @@ interface User {
|
|
|
25
25
|
user_id?: string;
|
|
26
26
|
[attr: string]: unknown;
|
|
27
27
|
}
|
|
28
|
-
interface ExperimentResult<P> {
|
|
29
|
-
inExperiment: boolean;
|
|
30
|
-
group: string;
|
|
31
|
-
params: P;
|
|
32
|
-
}
|
|
33
|
-
/** Options object form of `getExperiment` — the legacy `decode`/`variants`
|
|
34
|
-
* positional args plus per-call exposure control. */
|
|
35
|
-
interface GetExperimentOptions<P> {
|
|
36
|
-
/** Decode the raw stored params into the typed shape callers want. */
|
|
37
|
-
decode?: (raw: unknown) => P;
|
|
38
|
-
/** Variant-specific param overrides merged on top of group params. */
|
|
39
|
-
variants?: Record<string, Partial<P>>;
|
|
40
|
-
/**
|
|
41
|
-
* Override automatic exposure logging for this read. Defaults to the client's
|
|
42
|
-
* setting (`disableAutoExposure` flips it). `false` reads the variant without
|
|
43
|
-
* logging an exposure — pair with `logExposure(name)` at render time.
|
|
44
|
-
*/
|
|
45
|
-
logExposure?: boolean;
|
|
46
|
-
}
|
|
47
28
|
/**
|
|
48
29
|
* Why a flag evaluated the way it did (LaunchDarkly variationDetail parity).
|
|
49
30
|
* Computed at the client boundary:
|
|
@@ -75,11 +56,19 @@ interface EvalExpResult {
|
|
|
75
56
|
inExperiment: boolean;
|
|
76
57
|
group: string;
|
|
77
58
|
params: Record<string, unknown>;
|
|
59
|
+
/** The universe this experiment belongs to — lets `universe(name).assign()`
|
|
60
|
+
* find the enrolled experiment within a universe. */
|
|
61
|
+
universe?: string;
|
|
78
62
|
}
|
|
79
63
|
interface EvalResponse {
|
|
80
64
|
flags: Record<string, boolean>;
|
|
81
65
|
configs: Record<string, unknown>;
|
|
82
66
|
experiments: Record<string, EvalExpResult>;
|
|
67
|
+
/** Per-universe param defaults (name → default map) so `universe(name).get()`
|
|
68
|
+
* resolves to the universe default even when the unit is not enrolled. */
|
|
69
|
+
universes?: Record<string, {
|
|
70
|
+
defaults: Record<string, unknown>;
|
|
71
|
+
}>;
|
|
83
72
|
/**
|
|
84
73
|
* Killswitch state, flattened by the server. A boolean means the killswitch
|
|
85
74
|
* is whole-killed; an object means it's not whole-killed and carries per-
|
|
@@ -93,6 +82,23 @@ interface EvalResponse {
|
|
|
93
82
|
*/
|
|
94
83
|
sticky?: Record<string, StickyCookieEntry>;
|
|
95
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* The result of `universe(name).assign()` — the visitor's standing in a universe.
|
|
87
|
+
* A universe is a mutual-exclusion pool, so the visitor lands in **at most one**
|
|
88
|
+
* experiment. Never throws: an un-enrolled visitor still resolves `get()` to the
|
|
89
|
+
* universe defaults (or your fallback). `assign()` auto-logs one exposure when
|
|
90
|
+
* enrolled (subject to `disableAutoExposure`).
|
|
91
|
+
*/
|
|
92
|
+
interface Assignment {
|
|
93
|
+
/** The experiment the visitor landed in, or `null` when not enrolled. */
|
|
94
|
+
readonly name: string | null;
|
|
95
|
+
/** The assigned variant/group name, or `null` when not enrolled. */
|
|
96
|
+
readonly group: string | null;
|
|
97
|
+
/** True iff the visitor is enrolled in an experiment in this universe. */
|
|
98
|
+
readonly enrolled: boolean;
|
|
99
|
+
/** Read a resolved param: variant override ?? universe default ?? `fallback`. */
|
|
100
|
+
get<T = unknown>(field: string, fallback?: T): T | undefined;
|
|
101
|
+
}
|
|
96
102
|
/** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
|
|
97
103
|
interface StickyCookieEntry {
|
|
98
104
|
g: string;
|
|
@@ -104,6 +110,33 @@ interface AutoCollectGroups {
|
|
|
104
110
|
engagement: boolean;
|
|
105
111
|
}
|
|
106
112
|
type EngineEnv = "dev" | "staging" | "prod";
|
|
113
|
+
/**
|
|
114
|
+
* A pluggable persistence backend for the anonymous id. Primarily for non-DOM
|
|
115
|
+
* runtimes (React Native / Expo) where there is no cookie or `localStorage`, so
|
|
116
|
+
* the anon id would otherwise regenerate every app launch and re-bucket the
|
|
117
|
+
* visitor. Point it at `@react-native-async-storage/async-storage` (or any
|
|
118
|
+
* store) and the SDK hydrates a stable id before the first `/sdk/evaluate`:
|
|
119
|
+
*
|
|
120
|
+
* ```ts
|
|
121
|
+
* import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
122
|
+
* configure({
|
|
123
|
+
* clientKey: "...",
|
|
124
|
+
* anonymousStore: {
|
|
125
|
+
* get: (k) => AsyncStorage.getItem(k),
|
|
126
|
+
* set: (k, v) => AsyncStorage.setItem(k, v),
|
|
127
|
+
* remove: (k) => AsyncStorage.removeItem(k),
|
|
128
|
+
* },
|
|
129
|
+
* });
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* Methods may be sync or async — the SDK awaits either. Any thrown error is
|
|
133
|
+
* swallowed (the in-memory id is used as a fallback).
|
|
134
|
+
*/
|
|
135
|
+
interface AnonymousStore {
|
|
136
|
+
get(key: string): string | null | Promise<string | null>;
|
|
137
|
+
set(key: string, value: string): void | Promise<void>;
|
|
138
|
+
remove(key: string): void | Promise<void>;
|
|
139
|
+
}
|
|
107
140
|
interface EngineOptions {
|
|
108
141
|
sdkKey: string;
|
|
109
142
|
baseUrl?: string;
|
|
@@ -125,10 +158,26 @@ interface EngineOptions {
|
|
|
125
158
|
/** Which published env to read values from. Defaults to "prod". */
|
|
126
159
|
env?: EngineEnv;
|
|
127
160
|
/**
|
|
128
|
-
*
|
|
161
|
+
* Master network switch — when `false`, the browser SDK makes NO outbound
|
|
162
|
+
* requests at all: identify()/init never call /sdk/evaluate (getters read code
|
|
163
|
+
* defaults / overrides), track() and exposure logging are no-ops, usage
|
|
164
|
+
* telemetry is off, and SDK-internal error self-monitoring is off.
|
|
165
|
+
*
|
|
166
|
+
* DEFAULT is environment-derived (see {@link isProductionEnv}): ON in
|
|
167
|
+
* production, OFF everywhere else. In the browser there is usually no native
|
|
168
|
+
* `NODE_ENV`, so production is taken from the `env` option above (default
|
|
169
|
+
* `"prod"`) — meaning it defaults ON. Pass `false` (or set `env: "dev"`) to
|
|
170
|
+
* keep a local build quiet.
|
|
171
|
+
*/
|
|
172
|
+
isNetworkEnabled?: boolean;
|
|
173
|
+
/**
|
|
174
|
+
* Per-evaluation usage telemetry ("tracking") — each getFlag/getConfig/
|
|
129
175
|
* getExperiment/getKillswitch call fires one fire-and-forget sendBeacon so
|
|
130
|
-
* usage is counted by Cloudflare's native per-path analytics.
|
|
131
|
-
*
|
|
176
|
+
* usage is counted by Cloudflare's native per-path analytics.
|
|
177
|
+
*
|
|
178
|
+
* DEFAULT is environment-derived: ON in production, OFF everywhere else (same
|
|
179
|
+
* inference as {@link isNetworkEnabled}). Pass `true` to force it off, `false`
|
|
180
|
+
* to force it on. Forced off whenever `isNetworkEnabled` is false.
|
|
132
181
|
*/
|
|
133
182
|
disableTelemetry?: boolean;
|
|
134
183
|
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
@@ -141,6 +190,14 @@ interface EngineOptions {
|
|
|
141
190
|
* `silent` < `error` < `warn` < `info` < `debug`. Defaults to `"warn"`.
|
|
142
191
|
*/
|
|
143
192
|
logLevel?: LogLevel;
|
|
193
|
+
/**
|
|
194
|
+
* Opt out of SDK-internal error self-monitoring. When one of the SDK's own
|
|
195
|
+
* last-resort guards catches an internal failure (an "on our end" bug), the
|
|
196
|
+
* SDK reports it to Shipeasy's own project so we can track SDK bugs across
|
|
197
|
+
* apps — never to your project. ON by default; forced off in test mode. Pass
|
|
198
|
+
* `true` to disable.
|
|
199
|
+
*/
|
|
200
|
+
disableInternalErrorReporting?: boolean;
|
|
144
201
|
/**
|
|
145
202
|
* Suppress automatic exposure logging in `getExperiment` (Statsig's
|
|
146
203
|
* `disableExposureLogging`). Default false — reading an enrolled variant
|
|
@@ -164,6 +221,14 @@ interface EngineOptions {
|
|
|
164
221
|
* lever. Pass `false` to opt out (pure deterministic eval).
|
|
165
222
|
*/
|
|
166
223
|
stickyBucketing?: boolean;
|
|
224
|
+
/**
|
|
225
|
+
* Pluggable persistence for the anonymous id (React Native / Expo, or any
|
|
226
|
+
* runtime without a cookie / `localStorage`). When set, the SDK hydrates the
|
|
227
|
+
* stored id before the first `/sdk/evaluate` so bucketing stays stable across
|
|
228
|
+
* app launches; on a fresh device it persists the freshly-minted id. See
|
|
229
|
+
* {@link AnonymousStore}.
|
|
230
|
+
*/
|
|
231
|
+
anonymousStore?: AnonymousStore;
|
|
167
232
|
/**
|
|
168
233
|
* Test mode — no network at all. identify()/init are no-ops (never call
|
|
169
234
|
* /sdk/evaluate), track() is a no-op, telemetry is forced off, and the client
|
|
@@ -184,6 +249,7 @@ declare class Engine {
|
|
|
184
249
|
private readonly env;
|
|
185
250
|
private evalResult;
|
|
186
251
|
private anonId;
|
|
252
|
+
private anonReady;
|
|
187
253
|
private userId;
|
|
188
254
|
private buffer;
|
|
189
255
|
private telemetry;
|
|
@@ -193,11 +259,20 @@ declare class Engine {
|
|
|
193
259
|
private overrideListenerInstalled;
|
|
194
260
|
private identifySeq;
|
|
195
261
|
private readonly testMode;
|
|
262
|
+
private readonly networkEnabled;
|
|
263
|
+
private readonly offline;
|
|
196
264
|
private readonly flagOverrides;
|
|
197
265
|
private readonly configOverrides;
|
|
198
266
|
private readonly experimentOverrides;
|
|
199
267
|
private onOverrideChange;
|
|
200
268
|
constructor(opts: EngineOptions);
|
|
269
|
+
/**
|
|
270
|
+
* Consult the caller-supplied {@link AnonymousStore}: adopt a persisted id so
|
|
271
|
+
* the visitor buckets identically across app launches, or persist the id just
|
|
272
|
+
* minted by getOrCreateAnonId() on a fresh device. Best-effort — any store
|
|
273
|
+
* failure leaves the in-memory id in place.
|
|
274
|
+
*/
|
|
275
|
+
private hydrateAnonFromStore;
|
|
201
276
|
/**
|
|
202
277
|
* Build a no-network, immediately-usable browser client for tests
|
|
203
278
|
* (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
|
|
@@ -251,16 +326,21 @@ declare class Engine {
|
|
|
251
326
|
getFlag(name: string, defaultValue?: boolean): boolean;
|
|
252
327
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
253
328
|
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
254
|
-
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
|
|
255
|
-
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, opts: GetExperimentOptions<P>): ExperimentResult<P>;
|
|
256
329
|
/**
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* the
|
|
260
|
-
*
|
|
261
|
-
* `
|
|
330
|
+
* Assign the visitor within a universe: `universe("checkout").assign()`. A
|
|
331
|
+
* universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
|
|
332
|
+
* the returned {@link Assignment} exposes `.group` / `.get(field, fallback)`
|
|
333
|
+
* and auto-logs one exposure when enrolled (unless `disableAutoExposure` or
|
|
334
|
+
* `assign({ logExposure: false })`). An un-enrolled visitor still resolves
|
|
335
|
+
* `get()` to the universe defaults. This is the sole experiment read path —
|
|
336
|
+
* there is no `getExperiment` (ask a universe, not an experiment).
|
|
262
337
|
*/
|
|
263
|
-
|
|
338
|
+
universe(name: string): {
|
|
339
|
+
assign(opts?: {
|
|
340
|
+
logExposure?: boolean;
|
|
341
|
+
}): Assignment;
|
|
342
|
+
};
|
|
343
|
+
private assignUniverse;
|
|
264
344
|
/**
|
|
265
345
|
* Subscribe to state changes — fires after identify() completes and on
|
|
266
346
|
* `se:override:change` events from the devtools overlay. Returns an
|