@shipeasy/sdk 6.2.0 → 7.0.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 +147 -22
- package/dist/client/index.d.ts +147 -22
- package/dist/client/index.js +304 -125
- package/dist/client/index.mjs +303 -125
- package/dist/openfeature-server/index.d.mts +129 -8
- package/dist/openfeature-server/index.d.ts +129 -8
- package/dist/openfeature-server/index.js +66 -1
- package/dist/openfeature-server/index.mjs +66 -1
- package/dist/openfeature-web/index.d.mts +99 -27
- package/dist/openfeature-web/index.d.ts +99 -27
- package/dist/server/index.d.mts +166 -21
- package/dist/server/index.d.ts +166 -21
- package/dist/server/index.js +358 -131
- package/dist/server/index.mjs +357 -131
- package/docs/skill/SKILL.md +29 -25
- package/package.json +9 -6
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Provider, EvaluationContext, Logger, ResolutionDetails, JsonValue } from '@openfeature/server-sdk';
|
|
2
2
|
|
|
3
|
+
type LogLevel = "silent" | "error" | "warn" | "info" | "debug";
|
|
4
|
+
|
|
3
5
|
type SeeExtras = Record<string, string | number | boolean | null | undefined>;
|
|
4
6
|
type SeeKind = "caught" | "uncaught" | "unhandled_rejection" | "network" | "violation";
|
|
5
7
|
/** Built by `causesThe(subject).to(outcome)` — never constructed by hand. */
|
|
@@ -18,6 +20,10 @@ interface ExperimentResult<P> {
|
|
|
18
20
|
inExperiment: boolean;
|
|
19
21
|
group: string;
|
|
20
22
|
params: P;
|
|
23
|
+
/** The universe this experiment belongs to. Carried in the SSR bootstrap +
|
|
24
|
+
* `/sdk/evaluate` so the client can resolve `universe(name).assign()` by
|
|
25
|
+
* finding the enrolled experiment in a universe. */
|
|
26
|
+
universe?: string;
|
|
21
27
|
}
|
|
22
28
|
/**
|
|
23
29
|
* Why a flag evaluated the way it did (LaunchDarkly variationDetail parity).
|
|
@@ -63,13 +69,46 @@ interface ExperimentGroup {
|
|
|
63
69
|
interface Experiment {
|
|
64
70
|
universe: string;
|
|
65
71
|
targetingGate?: string | null;
|
|
72
|
+
/**
|
|
73
|
+
* Per-experiment holdout flag (a `type='holdout'` gate). When set and the flag
|
|
74
|
+
* passes for a unit, that unit is *held out* of THIS experiment — never
|
|
75
|
+
* assigned, sees the universe defaults. Checked after the universe holdout,
|
|
76
|
+
* before allocation. Mirrors @shipeasy/core §B3.
|
|
77
|
+
*/
|
|
78
|
+
holdoutGate?: string | null;
|
|
66
79
|
allocationPct: number;
|
|
80
|
+
/**
|
|
81
|
+
* Contiguous half-open slice `[poolOffsetBp, poolOffsetBp + poolSizeBp)` of the
|
|
82
|
+
* *universe* hash space this experiment claims. Used only when `hashVersion >= 2`
|
|
83
|
+
* (pooled assignment / real mutual exclusion, §B4): the unit's universe segment
|
|
84
|
+
* must fall in this range to be in the experiment. Null ⇒ fall back to the
|
|
85
|
+
* legacy independent-salt `allocationPct` gate.
|
|
86
|
+
*/
|
|
87
|
+
poolOffsetBp?: number | null;
|
|
88
|
+
poolSizeBp?: number | null;
|
|
89
|
+
/**
|
|
90
|
+
* A tail of the group split kept empty (basis points) so a new variant can be
|
|
91
|
+
* appended into it while running without reshuffling enrolled units. Group
|
|
92
|
+
* weights sum to `10000 − reservedHeadroomBp`; a unit hashing into the reserved
|
|
93
|
+
* tail is treated as not-assigned (sees universe defaults). §B5.
|
|
94
|
+
*/
|
|
95
|
+
reservedHeadroomBp?: number | null;
|
|
67
96
|
salt: string;
|
|
68
97
|
groups: ExperimentGroup[];
|
|
69
98
|
status: "draft" | "running" | "stopped" | "archived";
|
|
99
|
+
/** Bucketing scheme version. `>= 2` unlocks pooled (mutually-exclusive) assignment. */
|
|
100
|
+
hashVersion?: number;
|
|
70
101
|
/** Attribute to bucket on (e.g. company_id); defaults to user_id/anonymous_id. */
|
|
71
102
|
bucketBy?: string | null;
|
|
72
103
|
}
|
|
104
|
+
/** One param the universe owns: a name, a type, and the default a variant
|
|
105
|
+
* inherits when it doesn't override the value (§A/§B2). */
|
|
106
|
+
interface UniverseParam {
|
|
107
|
+
name: string;
|
|
108
|
+
type: "bool" | "int" | "number" | "string";
|
|
109
|
+
default: unknown;
|
|
110
|
+
}
|
|
111
|
+
type UniverseParamSchema = UniverseParam[];
|
|
73
112
|
/** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
|
|
74
113
|
interface StickyEntry {
|
|
75
114
|
g: string;
|
|
@@ -88,6 +127,13 @@ interface StickyBucketStore {
|
|
|
88
127
|
}
|
|
89
128
|
interface Universe {
|
|
90
129
|
holdout_range: [number, number] | null;
|
|
130
|
+
/**
|
|
131
|
+
* The universe's config schema — the single source of truth for param names,
|
|
132
|
+
* types, and defaults (§A). The `assign()` merge layers these defaults *under*
|
|
133
|
+
* an assigned variant's overrides so an unset param still returns the universe
|
|
134
|
+
* default. Null/absent ⇒ no defaults (legacy: variant params returned verbatim).
|
|
135
|
+
*/
|
|
136
|
+
param_schema?: UniverseParamSchema | null;
|
|
91
137
|
}
|
|
92
138
|
interface Killswitch {
|
|
93
139
|
killed: 0 | 1 | boolean;
|
|
@@ -114,6 +160,34 @@ interface BootstrapPayload {
|
|
|
114
160
|
configs: Record<string, unknown>;
|
|
115
161
|
experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
|
|
116
162
|
killswitches: Record<string, boolean | Record<string, boolean>>;
|
|
163
|
+
/** Per-universe param defaults (name → default map) so the client can resolve
|
|
164
|
+
* `universe(name).get(field)` to the universe default even when the unit is
|
|
165
|
+
* not enrolled in any experiment there. Only universes with running
|
|
166
|
+
* experiments are included. */
|
|
167
|
+
universes?: Record<string, {
|
|
168
|
+
defaults: Record<string, unknown>;
|
|
169
|
+
}>;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The result of `universe(name).assign(user)` — a user's standing in a universe.
|
|
173
|
+
* A universe is a mutual-exclusion pool, so a unit lands in **at most one**
|
|
174
|
+
* experiment. Never throws: an un-enrolled unit still resolves `get()` to the
|
|
175
|
+
* universe defaults (or your fallback). Reading is side-effect free — the single
|
|
176
|
+
* exposure is logged once by `assign()` when the unit is enrolled.
|
|
177
|
+
*/
|
|
178
|
+
interface Assignment {
|
|
179
|
+
/** The experiment the unit landed in, or `null` when not enrolled. */
|
|
180
|
+
readonly name: string | null;
|
|
181
|
+
/** The assigned variant/group name, or `null` when not enrolled. */
|
|
182
|
+
readonly group: string | null;
|
|
183
|
+
/** True iff the unit is enrolled in an experiment in this universe. */
|
|
184
|
+
readonly enrolled: boolean;
|
|
185
|
+
/**
|
|
186
|
+
* Read a resolved param: the assigned variant's override, else the universe
|
|
187
|
+
* default, else `fallback`. Works even when not enrolled (variant layer is
|
|
188
|
+
* absent, so you get `universeDefault ?? fallback`).
|
|
189
|
+
*/
|
|
190
|
+
get<T = unknown>(field: string, fallback?: T): T | undefined;
|
|
117
191
|
}
|
|
118
192
|
type EngineEnv = "dev" | "staging" | "prod";
|
|
119
193
|
interface EngineOptions {
|
|
@@ -137,6 +211,24 @@ interface EngineOptions {
|
|
|
137
211
|
disableTelemetry?: boolean;
|
|
138
212
|
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
139
213
|
telemetryUrl?: string;
|
|
214
|
+
/**
|
|
215
|
+
* How chatty the SDK is on `console` when it catches an error internally.
|
|
216
|
+
* Every public runtime method (getFlag/getConfig/getExperiment/getKillswitch/
|
|
217
|
+
* track/logExposure/see) fails silently — it returns a safe default rather
|
|
218
|
+
* than throwing — and surfaces the swallowed error through this level so you
|
|
219
|
+
* still find out. Ordering: `silent` < `error` < `warn` < `info` < `debug`.
|
|
220
|
+
* Defaults to `"warn"` (prints `error` + `warn`). Pass `"silent"` to mute the
|
|
221
|
+
* SDK entirely. See {@link LogLevel}.
|
|
222
|
+
*/
|
|
223
|
+
logLevel?: LogLevel;
|
|
224
|
+
/**
|
|
225
|
+
* Opt out of SDK-internal error self-monitoring. When one of the SDK's own
|
|
226
|
+
* last-resort guards catches an internal failure (an "on our end" bug), the
|
|
227
|
+
* SDK reports it to Shipeasy's own project so we can track SDK bugs across
|
|
228
|
+
* apps — never to your project. ON by default; forced off in test mode. Pass
|
|
229
|
+
* `true` to disable.
|
|
230
|
+
*/
|
|
231
|
+
disableInternalErrorReporting?: boolean;
|
|
140
232
|
/**
|
|
141
233
|
* Attribute names usable for targeting but never persisted in analytics
|
|
142
234
|
* (LD/Statsig `privateAttributes`). The server evaluates locally so private
|
|
@@ -179,6 +271,7 @@ declare class Engine {
|
|
|
179
271
|
private readonly flagOverrides;
|
|
180
272
|
private readonly configOverrides;
|
|
181
273
|
private readonly experimentOverrides;
|
|
274
|
+
private readonly exposureSeen;
|
|
182
275
|
private readonly changeListeners;
|
|
183
276
|
constructor(opts: EngineOptions);
|
|
184
277
|
/**
|
|
@@ -258,19 +351,47 @@ declare class Engine {
|
|
|
258
351
|
getFlag(name: string, user: User, defaultValue?: boolean): boolean;
|
|
259
352
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
260
353
|
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
261
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Bind the per-experiment sticky seam over the configured {@link StickyBucketStore}
|
|
356
|
+
* for one (unit, experiment). Absent store ⇒ deterministic (no sticky).
|
|
357
|
+
*/
|
|
358
|
+
private bindSticky;
|
|
359
|
+
/**
|
|
360
|
+
* Evaluate one experiment by name for `user` — override → full classify
|
|
361
|
+
* pipeline (targeting → universe holdout → holdout gate → sticky → allocation
|
|
362
|
+
* → group), merging the universe defaults under the assigned variant (§B2).
|
|
363
|
+
* Internal: the public surface is `universe(name).assign(user)`. Reused by the
|
|
364
|
+
* SSR `evaluate()` bootstrap (keyed by experiment name) and by `assignUniverse`.
|
|
365
|
+
*/
|
|
366
|
+
private evalExperiment;
|
|
367
|
+
/**
|
|
368
|
+
* Assign `user` within `universeName`. A universe is a mutual-exclusion pool,
|
|
369
|
+
* so a unit lands in **at most one** experiment; the returned {@link Assignment}
|
|
370
|
+
* exposes the variant + resolved params and auto-logs a single exposure when
|
|
371
|
+
* enrolled. An un-enrolled unit still resolves `get()` to the universe defaults.
|
|
372
|
+
* Never throws. This is the sole experiment read path (there is no
|
|
373
|
+
* `getExperiment` — a caller asks a universe, not an experiment).
|
|
374
|
+
*/
|
|
375
|
+
assignUniverse(universeName: string, user: User): Assignment;
|
|
376
|
+
/**
|
|
377
|
+
* The universe-first experiment read entry point:
|
|
378
|
+
* `engine.universe("checkout").assign(user)`. Returns a reusable handle bound
|
|
379
|
+
* to one universe; `assign(user)` picks the ≤1 experiment the unit is pooled
|
|
380
|
+
* into and auto-logs a single exposure. See {@link assignUniverse}.
|
|
381
|
+
*/
|
|
382
|
+
universe(name: string): {
|
|
383
|
+
assign(user: User): Assignment;
|
|
384
|
+
};
|
|
262
385
|
/** Drop caller-marked private attributes from an outbound props bag. */
|
|
263
386
|
private stripPrivate;
|
|
264
387
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
265
388
|
/**
|
|
266
|
-
*
|
|
267
|
-
* (
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
* as `{ user_id }`); if the user is enrolled, POSTs a single exposure to
|
|
271
|
-
* `/collect`. No-op in test mode or when the user isn't enrolled.
|
|
389
|
+
* POST a single exposure for an enrolled `(user, experiment, group)`. Deduped
|
|
390
|
+
* per process (bounded set) so repeated `assign()` calls in one server don't
|
|
391
|
+
* spam `/collect`. Fire-and-forget; no-op in test mode. This is how
|
|
392
|
+
* `assignUniverse` auto-logs — the browser's auto-exposure parity for SSR.
|
|
272
393
|
*/
|
|
273
|
-
|
|
394
|
+
private postExposure;
|
|
274
395
|
/**
|
|
275
396
|
* Report a structured error into the errors primitive. Fire-and-forget —
|
|
276
397
|
* never blocks or throws into the request path. Spam-guarded by a 30s
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Provider, EvaluationContext, Logger, ResolutionDetails, JsonValue } from '@openfeature/server-sdk';
|
|
2
2
|
|
|
3
|
+
type LogLevel = "silent" | "error" | "warn" | "info" | "debug";
|
|
4
|
+
|
|
3
5
|
type SeeExtras = Record<string, string | number | boolean | null | undefined>;
|
|
4
6
|
type SeeKind = "caught" | "uncaught" | "unhandled_rejection" | "network" | "violation";
|
|
5
7
|
/** Built by `causesThe(subject).to(outcome)` — never constructed by hand. */
|
|
@@ -18,6 +20,10 @@ interface ExperimentResult<P> {
|
|
|
18
20
|
inExperiment: boolean;
|
|
19
21
|
group: string;
|
|
20
22
|
params: P;
|
|
23
|
+
/** The universe this experiment belongs to. Carried in the SSR bootstrap +
|
|
24
|
+
* `/sdk/evaluate` so the client can resolve `universe(name).assign()` by
|
|
25
|
+
* finding the enrolled experiment in a universe. */
|
|
26
|
+
universe?: string;
|
|
21
27
|
}
|
|
22
28
|
/**
|
|
23
29
|
* Why a flag evaluated the way it did (LaunchDarkly variationDetail parity).
|
|
@@ -63,13 +69,46 @@ interface ExperimentGroup {
|
|
|
63
69
|
interface Experiment {
|
|
64
70
|
universe: string;
|
|
65
71
|
targetingGate?: string | null;
|
|
72
|
+
/**
|
|
73
|
+
* Per-experiment holdout flag (a `type='holdout'` gate). When set and the flag
|
|
74
|
+
* passes for a unit, that unit is *held out* of THIS experiment — never
|
|
75
|
+
* assigned, sees the universe defaults. Checked after the universe holdout,
|
|
76
|
+
* before allocation. Mirrors @shipeasy/core §B3.
|
|
77
|
+
*/
|
|
78
|
+
holdoutGate?: string | null;
|
|
66
79
|
allocationPct: number;
|
|
80
|
+
/**
|
|
81
|
+
* Contiguous half-open slice `[poolOffsetBp, poolOffsetBp + poolSizeBp)` of the
|
|
82
|
+
* *universe* hash space this experiment claims. Used only when `hashVersion >= 2`
|
|
83
|
+
* (pooled assignment / real mutual exclusion, §B4): the unit's universe segment
|
|
84
|
+
* must fall in this range to be in the experiment. Null ⇒ fall back to the
|
|
85
|
+
* legacy independent-salt `allocationPct` gate.
|
|
86
|
+
*/
|
|
87
|
+
poolOffsetBp?: number | null;
|
|
88
|
+
poolSizeBp?: number | null;
|
|
89
|
+
/**
|
|
90
|
+
* A tail of the group split kept empty (basis points) so a new variant can be
|
|
91
|
+
* appended into it while running without reshuffling enrolled units. Group
|
|
92
|
+
* weights sum to `10000 − reservedHeadroomBp`; a unit hashing into the reserved
|
|
93
|
+
* tail is treated as not-assigned (sees universe defaults). §B5.
|
|
94
|
+
*/
|
|
95
|
+
reservedHeadroomBp?: number | null;
|
|
67
96
|
salt: string;
|
|
68
97
|
groups: ExperimentGroup[];
|
|
69
98
|
status: "draft" | "running" | "stopped" | "archived";
|
|
99
|
+
/** Bucketing scheme version. `>= 2` unlocks pooled (mutually-exclusive) assignment. */
|
|
100
|
+
hashVersion?: number;
|
|
70
101
|
/** Attribute to bucket on (e.g. company_id); defaults to user_id/anonymous_id. */
|
|
71
102
|
bucketBy?: string | null;
|
|
72
103
|
}
|
|
104
|
+
/** One param the universe owns: a name, a type, and the default a variant
|
|
105
|
+
* inherits when it doesn't override the value (§A/§B2). */
|
|
106
|
+
interface UniverseParam {
|
|
107
|
+
name: string;
|
|
108
|
+
type: "bool" | "int" | "number" | "string";
|
|
109
|
+
default: unknown;
|
|
110
|
+
}
|
|
111
|
+
type UniverseParamSchema = UniverseParam[];
|
|
73
112
|
/** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
|
|
74
113
|
interface StickyEntry {
|
|
75
114
|
g: string;
|
|
@@ -88,6 +127,13 @@ interface StickyBucketStore {
|
|
|
88
127
|
}
|
|
89
128
|
interface Universe {
|
|
90
129
|
holdout_range: [number, number] | null;
|
|
130
|
+
/**
|
|
131
|
+
* The universe's config schema — the single source of truth for param names,
|
|
132
|
+
* types, and defaults (§A). The `assign()` merge layers these defaults *under*
|
|
133
|
+
* an assigned variant's overrides so an unset param still returns the universe
|
|
134
|
+
* default. Null/absent ⇒ no defaults (legacy: variant params returned verbatim).
|
|
135
|
+
*/
|
|
136
|
+
param_schema?: UniverseParamSchema | null;
|
|
91
137
|
}
|
|
92
138
|
interface Killswitch {
|
|
93
139
|
killed: 0 | 1 | boolean;
|
|
@@ -114,6 +160,34 @@ interface BootstrapPayload {
|
|
|
114
160
|
configs: Record<string, unknown>;
|
|
115
161
|
experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
|
|
116
162
|
killswitches: Record<string, boolean | Record<string, boolean>>;
|
|
163
|
+
/** Per-universe param defaults (name → default map) so the client can resolve
|
|
164
|
+
* `universe(name).get(field)` to the universe default even when the unit is
|
|
165
|
+
* not enrolled in any experiment there. Only universes with running
|
|
166
|
+
* experiments are included. */
|
|
167
|
+
universes?: Record<string, {
|
|
168
|
+
defaults: Record<string, unknown>;
|
|
169
|
+
}>;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The result of `universe(name).assign(user)` — a user's standing in a universe.
|
|
173
|
+
* A universe is a mutual-exclusion pool, so a unit lands in **at most one**
|
|
174
|
+
* experiment. Never throws: an un-enrolled unit still resolves `get()` to the
|
|
175
|
+
* universe defaults (or your fallback). Reading is side-effect free — the single
|
|
176
|
+
* exposure is logged once by `assign()` when the unit is enrolled.
|
|
177
|
+
*/
|
|
178
|
+
interface Assignment {
|
|
179
|
+
/** The experiment the unit landed in, or `null` when not enrolled. */
|
|
180
|
+
readonly name: string | null;
|
|
181
|
+
/** The assigned variant/group name, or `null` when not enrolled. */
|
|
182
|
+
readonly group: string | null;
|
|
183
|
+
/** True iff the unit is enrolled in an experiment in this universe. */
|
|
184
|
+
readonly enrolled: boolean;
|
|
185
|
+
/**
|
|
186
|
+
* Read a resolved param: the assigned variant's override, else the universe
|
|
187
|
+
* default, else `fallback`. Works even when not enrolled (variant layer is
|
|
188
|
+
* absent, so you get `universeDefault ?? fallback`).
|
|
189
|
+
*/
|
|
190
|
+
get<T = unknown>(field: string, fallback?: T): T | undefined;
|
|
117
191
|
}
|
|
118
192
|
type EngineEnv = "dev" | "staging" | "prod";
|
|
119
193
|
interface EngineOptions {
|
|
@@ -137,6 +211,24 @@ interface EngineOptions {
|
|
|
137
211
|
disableTelemetry?: boolean;
|
|
138
212
|
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
139
213
|
telemetryUrl?: string;
|
|
214
|
+
/**
|
|
215
|
+
* How chatty the SDK is on `console` when it catches an error internally.
|
|
216
|
+
* Every public runtime method (getFlag/getConfig/getExperiment/getKillswitch/
|
|
217
|
+
* track/logExposure/see) fails silently — it returns a safe default rather
|
|
218
|
+
* than throwing — and surfaces the swallowed error through this level so you
|
|
219
|
+
* still find out. Ordering: `silent` < `error` < `warn` < `info` < `debug`.
|
|
220
|
+
* Defaults to `"warn"` (prints `error` + `warn`). Pass `"silent"` to mute the
|
|
221
|
+
* SDK entirely. See {@link LogLevel}.
|
|
222
|
+
*/
|
|
223
|
+
logLevel?: LogLevel;
|
|
224
|
+
/**
|
|
225
|
+
* Opt out of SDK-internal error self-monitoring. When one of the SDK's own
|
|
226
|
+
* last-resort guards catches an internal failure (an "on our end" bug), the
|
|
227
|
+
* SDK reports it to Shipeasy's own project so we can track SDK bugs across
|
|
228
|
+
* apps — never to your project. ON by default; forced off in test mode. Pass
|
|
229
|
+
* `true` to disable.
|
|
230
|
+
*/
|
|
231
|
+
disableInternalErrorReporting?: boolean;
|
|
140
232
|
/**
|
|
141
233
|
* Attribute names usable for targeting but never persisted in analytics
|
|
142
234
|
* (LD/Statsig `privateAttributes`). The server evaluates locally so private
|
|
@@ -179,6 +271,7 @@ declare class Engine {
|
|
|
179
271
|
private readonly flagOverrides;
|
|
180
272
|
private readonly configOverrides;
|
|
181
273
|
private readonly experimentOverrides;
|
|
274
|
+
private readonly exposureSeen;
|
|
182
275
|
private readonly changeListeners;
|
|
183
276
|
constructor(opts: EngineOptions);
|
|
184
277
|
/**
|
|
@@ -258,19 +351,47 @@ declare class Engine {
|
|
|
258
351
|
getFlag(name: string, user: User, defaultValue?: boolean): boolean;
|
|
259
352
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
260
353
|
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
261
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Bind the per-experiment sticky seam over the configured {@link StickyBucketStore}
|
|
356
|
+
* for one (unit, experiment). Absent store ⇒ deterministic (no sticky).
|
|
357
|
+
*/
|
|
358
|
+
private bindSticky;
|
|
359
|
+
/**
|
|
360
|
+
* Evaluate one experiment by name for `user` — override → full classify
|
|
361
|
+
* pipeline (targeting → universe holdout → holdout gate → sticky → allocation
|
|
362
|
+
* → group), merging the universe defaults under the assigned variant (§B2).
|
|
363
|
+
* Internal: the public surface is `universe(name).assign(user)`. Reused by the
|
|
364
|
+
* SSR `evaluate()` bootstrap (keyed by experiment name) and by `assignUniverse`.
|
|
365
|
+
*/
|
|
366
|
+
private evalExperiment;
|
|
367
|
+
/**
|
|
368
|
+
* Assign `user` within `universeName`. A universe is a mutual-exclusion pool,
|
|
369
|
+
* so a unit lands in **at most one** experiment; the returned {@link Assignment}
|
|
370
|
+
* exposes the variant + resolved params and auto-logs a single exposure when
|
|
371
|
+
* enrolled. An un-enrolled unit still resolves `get()` to the universe defaults.
|
|
372
|
+
* Never throws. This is the sole experiment read path (there is no
|
|
373
|
+
* `getExperiment` — a caller asks a universe, not an experiment).
|
|
374
|
+
*/
|
|
375
|
+
assignUniverse(universeName: string, user: User): Assignment;
|
|
376
|
+
/**
|
|
377
|
+
* The universe-first experiment read entry point:
|
|
378
|
+
* `engine.universe("checkout").assign(user)`. Returns a reusable handle bound
|
|
379
|
+
* to one universe; `assign(user)` picks the ≤1 experiment the unit is pooled
|
|
380
|
+
* into and auto-logs a single exposure. See {@link assignUniverse}.
|
|
381
|
+
*/
|
|
382
|
+
universe(name: string): {
|
|
383
|
+
assign(user: User): Assignment;
|
|
384
|
+
};
|
|
262
385
|
/** Drop caller-marked private attributes from an outbound props bag. */
|
|
263
386
|
private stripPrivate;
|
|
264
387
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
265
388
|
/**
|
|
266
|
-
*
|
|
267
|
-
* (
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
* as `{ user_id }`); if the user is enrolled, POSTs a single exposure to
|
|
271
|
-
* `/collect`. No-op in test mode or when the user isn't enrolled.
|
|
389
|
+
* POST a single exposure for an enrolled `(user, experiment, group)`. Deduped
|
|
390
|
+
* per process (bounded set) so repeated `assign()` calls in one server don't
|
|
391
|
+
* spam `/collect`. Fire-and-forget; no-op in test mode. This is how
|
|
392
|
+
* `assignUniverse` auto-logs — the browser's auto-exposure parity for SSR.
|
|
272
393
|
*/
|
|
273
|
-
|
|
394
|
+
private postExposure;
|
|
274
395
|
/**
|
|
275
396
|
* Report a structured error into the errors primitive. Fire-and-forget —
|
|
276
397
|
* never blocks or throws into the request path. Spam-guarded by a 30s
|
|
@@ -32,6 +32,8 @@ var import_node_async_hooks = require("async_hooks");
|
|
|
32
32
|
var SEE_MAX_SUBJECT = 200;
|
|
33
33
|
var SEE_MAX_EXTRA_VALUE = 200;
|
|
34
34
|
var SEE_MAX_EXTRA_KEYS = 20;
|
|
35
|
+
var SEE_DEDUP_WINDOW_MS = 3e4;
|
|
36
|
+
var SEE_MAX_PER_SESSION = 25;
|
|
35
37
|
function causesThe(subject) {
|
|
36
38
|
return {
|
|
37
39
|
to(outcome) {
|
|
@@ -142,6 +144,69 @@ function startControlFlowChain(err) {
|
|
|
142
144
|
}
|
|
143
145
|
};
|
|
144
146
|
}
|
|
147
|
+
function topStackLine(stack) {
|
|
148
|
+
if (!stack) return "";
|
|
149
|
+
for (const line of stack.split("\n")) {
|
|
150
|
+
if (/^\s*at |@|:\d+:\d+/.test(line)) return line.trim().slice(0, 200);
|
|
151
|
+
}
|
|
152
|
+
return "";
|
|
153
|
+
}
|
|
154
|
+
var SeeLimiter = class {
|
|
155
|
+
constructor(maxPerSession = SEE_MAX_PER_SESSION, dedupWindowMs = SEE_DEDUP_WINDOW_MS) {
|
|
156
|
+
this.maxPerSession = maxPerSession;
|
|
157
|
+
this.dedupWindowMs = dedupWindowMs;
|
|
158
|
+
}
|
|
159
|
+
maxPerSession;
|
|
160
|
+
dedupWindowMs;
|
|
161
|
+
lastSent = /* @__PURE__ */ new Map();
|
|
162
|
+
sent = 0;
|
|
163
|
+
shouldSend(ev) {
|
|
164
|
+
if (this.sent >= this.maxPerSession) return false;
|
|
165
|
+
const key = `${ev.kind}|${ev.error_type}|${ev.message.slice(0, 200)}|${topStackLine(ev.stack)}`;
|
|
166
|
+
const now = Date.now();
|
|
167
|
+
const prev = this.lastSent.get(key);
|
|
168
|
+
if (prev !== void 0 && now - prev < this.dedupWindowMs) return false;
|
|
169
|
+
this.lastSent.set(key, now);
|
|
170
|
+
this.sent += 1;
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// src/internal-report.ts
|
|
176
|
+
var limiter = new SeeLimiter();
|
|
177
|
+
|
|
178
|
+
// src/logger.ts
|
|
179
|
+
var RANK = {
|
|
180
|
+
silent: 0,
|
|
181
|
+
error: 1,
|
|
182
|
+
warn: 2,
|
|
183
|
+
info: 3,
|
|
184
|
+
debug: 4
|
|
185
|
+
};
|
|
186
|
+
var currentLevel = "warn";
|
|
187
|
+
function write(method, args) {
|
|
188
|
+
try {
|
|
189
|
+
const c = globalThis.console;
|
|
190
|
+
if (!c) return;
|
|
191
|
+
const fn = c[method] ?? c.log;
|
|
192
|
+
if (typeof fn === "function") fn.call(c, ...args);
|
|
193
|
+
} catch {
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
var logger = {
|
|
197
|
+
error(...args) {
|
|
198
|
+
if (RANK[currentLevel] >= RANK.error) write("error", args);
|
|
199
|
+
},
|
|
200
|
+
warn(...args) {
|
|
201
|
+
if (RANK[currentLevel] >= RANK.warn) write("warn", args);
|
|
202
|
+
},
|
|
203
|
+
info(...args) {
|
|
204
|
+
if (RANK[currentLevel] >= RANK.info) write("info", args);
|
|
205
|
+
},
|
|
206
|
+
debug(...args) {
|
|
207
|
+
if (RANK[currentLevel] >= RANK.debug) write("debug", args);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
145
210
|
|
|
146
211
|
// src/server/index.ts
|
|
147
212
|
var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
|
|
@@ -183,7 +248,7 @@ function getShipeasyServerClient() {
|
|
|
183
248
|
}
|
|
184
249
|
function dispatchSee(problem, consequence, extras, kind) {
|
|
185
250
|
if (!_server) {
|
|
186
|
-
|
|
251
|
+
logger.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
|
|
187
252
|
return;
|
|
188
253
|
}
|
|
189
254
|
_server.reportError(problem, consequence, extras, kind);
|
|
@@ -8,6 +8,8 @@ import { AsyncLocalStorage } from "async_hooks";
|
|
|
8
8
|
var SEE_MAX_SUBJECT = 200;
|
|
9
9
|
var SEE_MAX_EXTRA_VALUE = 200;
|
|
10
10
|
var SEE_MAX_EXTRA_KEYS = 20;
|
|
11
|
+
var SEE_DEDUP_WINDOW_MS = 3e4;
|
|
12
|
+
var SEE_MAX_PER_SESSION = 25;
|
|
11
13
|
function causesThe(subject) {
|
|
12
14
|
return {
|
|
13
15
|
to(outcome) {
|
|
@@ -118,6 +120,69 @@ function startControlFlowChain(err) {
|
|
|
118
120
|
}
|
|
119
121
|
};
|
|
120
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
|
+
};
|
|
121
186
|
|
|
122
187
|
// src/server/index.ts
|
|
123
188
|
var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
|
|
@@ -159,7 +224,7 @@ function getShipeasyServerClient() {
|
|
|
159
224
|
}
|
|
160
225
|
function dispatchSee(problem, consequence, extras, kind) {
|
|
161
226
|
if (!_server) {
|
|
162
|
-
|
|
227
|
+
logger.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
|
|
163
228
|
return;
|
|
164
229
|
}
|
|
165
230
|
_server.reportError(problem, consequence, extras, kind);
|