@avsbhq/node 1.6.2 → 1.7.1
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/LICENSE +21 -0
- package/README.md +576 -272
- package/dist/index.cjs +946 -523
- package/dist/index.d.cts +399 -135
- package/dist/index.d.ts +399 -135
- package/dist/index.js +920 -507
- package/package.json +33 -11
package/dist/index.d.ts
CHANGED
|
@@ -1,54 +1,172 @@
|
|
|
1
|
-
import { FlagDatafile, StickyBucketService,
|
|
1
|
+
import { LogLevel, Logger, FlagDatafile, StickyBucketService, DecideOption, DecisionLog, Flag, ExposureEvent, StickyAssignment, TrackPayload, EvalContext, InitResult } from '@avsbhq/core';
|
|
2
2
|
export { DatafileDataset, DatafileDatasetType, DecideOption, DecisionLog, DecisionLogEntry, EvalContext, EvaluationSource, ExposureEvent, Flag, FlagDatafile, FlagEvaluationDetails, InitResult, Logger, MetricEvent, MultiContext, PurchaseHistoryAttribute, RuleType, SingleContext, StickyAssignment, StickyBucketService, TrackPayload, consoleLogger, consoleTransport, createLogger, noopLogger } from '@avsbhq/core';
|
|
3
3
|
import { AvsbUtils } from '@avsbhq/utils';
|
|
4
4
|
export { AvsbUtils } from '@avsbhq/utils';
|
|
5
|
+
export { ExpressAvsbServer, ExpressMiddlewareOptions, expressMiddleware } from '@avsbhq/utils/middleware/express';
|
|
6
|
+
export { FastifyAvsbServer, FastifyPluginOptions, fastifyPlugin } from '@avsbhq/utils/middleware/fastify';
|
|
5
7
|
export { StreamingClient, StreamingOptions, createStreamingClient } from '@avsbhq/utils/streaming';
|
|
6
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Server-side datasets read client (e-commerce suite Phase 2).
|
|
11
|
+
*
|
|
12
|
+
* Resolves the live version of a dataset from the SDK's datafile
|
|
13
|
+
* (`datasets` field) and fetches a single key from the edge worker:
|
|
14
|
+
*
|
|
15
|
+
* GET {ingestionHost}/v1/ds/{sdkKey}/{slug}/{versionShortId}/{key}
|
|
16
|
+
*
|
|
17
|
+
* The ingestion host is derived from the datafile's `collectEndpoint`,
|
|
18
|
+
* exactly like the server tracker derives `/v1/collect/*`. The worker's
|
|
19
|
+
* key cache resolves SDK keys as well as snippet keys in the first path
|
|
20
|
+
* segment.
|
|
21
|
+
*
|
|
22
|
+
* Error posture matches flag evaluation: this client NEVER throws.
|
|
23
|
+
* Unknown slug, missing live version, SDK not ready, HTTP errors and
|
|
24
|
+
* network failures all degrade to `{ found: false }`.
|
|
25
|
+
*/
|
|
26
|
+
interface DatasetGetResult {
|
|
27
|
+
found: boolean;
|
|
28
|
+
/** Parsed row for TABLE/FEED datasets; `true` for LIST membership hits. */
|
|
29
|
+
value?: unknown;
|
|
30
|
+
meta: {
|
|
31
|
+
slug: string;
|
|
32
|
+
/** Live version short id used for the lookup; null when unresolvable. */
|
|
33
|
+
version: number | null;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
interface DatasetsClient {
|
|
37
|
+
/**
|
|
38
|
+
* Look up `key` in the live version of dataset `slug`.
|
|
39
|
+
* Resolves the version from the current datafile and never throws.
|
|
40
|
+
*/
|
|
41
|
+
get(slug: string, key: string): Promise<DatasetGetResult>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** `logLevel` option values. 'silent' selects the noop logger. */
|
|
45
|
+
type ServerLogLevel = LogLevel | 'silent';
|
|
46
|
+
/**
|
|
47
|
+
* True when this process looks like a development environment. Exported so
|
|
48
|
+
* the behaviour is directly testable and so a caller can mirror the decision.
|
|
49
|
+
*/
|
|
50
|
+
declare function isDevEnvironment(): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* The logger used when the caller passed neither `logger` nor `logLevel`:
|
|
53
|
+
* console at warn level in development, noop otherwise.
|
|
54
|
+
*/
|
|
55
|
+
declare function defaultLogger(): Logger;
|
|
56
|
+
/** Resolve the constructor's logging options into one Logger. */
|
|
57
|
+
declare function resolveServerLogger(options: {
|
|
58
|
+
logger?: Logger;
|
|
59
|
+
logLevel?: ServerLogLevel;
|
|
60
|
+
}): Logger;
|
|
61
|
+
|
|
7
62
|
interface AvsbServerOptions {
|
|
8
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* SDK key for the environment, shaped `sdk_<environment>_<id>`, for example
|
|
65
|
+
* `sdk_production_ttqm0eaj4vth1krcb2xn`. Copy it from app.avsb.cloud, then
|
|
66
|
+
* Settings, then Environments. The shape is checked at construction and an
|
|
67
|
+
* unrecognisable value is logged (never thrown) with what looked wrong.
|
|
68
|
+
*/
|
|
9
69
|
sdkKey: string;
|
|
10
70
|
/** Datafile polling interval in ms. Default: 60_000 (60s). Min: 5_000. */
|
|
11
71
|
pollingInterval?: number;
|
|
12
72
|
/** CDN host for datafile fetches. Default: "https://cdn.avsb.cloud" */
|
|
13
73
|
cdnHost?: string;
|
|
14
|
-
/** Max ms to wait for initial datafile fetch in onReady(). Default: 10_000. */
|
|
74
|
+
/** Max ms to wait for the initial datafile fetch in onReady(). Default: 10_000. */
|
|
15
75
|
initTimeout?: number;
|
|
16
|
-
/** Pre-fetched datafile
|
|
76
|
+
/** Pre-fetched datafile, so onReady() resolves without a network request. */
|
|
17
77
|
bootstrap?: FlagDatafile;
|
|
18
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Real-time datafile updates over SSE. Default: TRUE on the server
|
|
80
|
+
* (breaking change B12): server connection counts are low, one SSE stream
|
|
81
|
+
* per process is cheaper than polling, and flag changes then land instantly.
|
|
82
|
+
*
|
|
83
|
+
* Falling back is invisible: if the datafile carries no `streamEndpoint` and
|
|
84
|
+
* no `streamingEndpoint` option was passed, or the stream cannot be opened,
|
|
85
|
+
* the SDK keeps polling and says so at debug level. Flags still update, just
|
|
86
|
+
* on the polling interval. Pass `false` to force polling only.
|
|
87
|
+
*/
|
|
19
88
|
streaming?: boolean;
|
|
20
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* Stream TICKET endpoint. Overrides the datafile's `streamEndpoint` field.
|
|
91
|
+
* Only needed for a self-hosted or proxied deployment whose publisher does
|
|
92
|
+
* not set the field; nothing is ever guessed from another host.
|
|
93
|
+
*/
|
|
21
94
|
streamingEndpoint?: string;
|
|
22
95
|
/** Event tracking configuration. */
|
|
23
96
|
events?: EventOptions;
|
|
24
97
|
/** Pluggable sticky bucketing service. */
|
|
25
98
|
stickyBucketService?: StickyBucketService;
|
|
26
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* Custom logger. Default: console at warn level when NODE_ENV is neither
|
|
101
|
+
* 'production' nor 'test', noop otherwise.
|
|
102
|
+
*/
|
|
27
103
|
logger?: Logger;
|
|
104
|
+
/**
|
|
105
|
+
* Shorthand for the default logger's level. `'silent'` restores the old
|
|
106
|
+
* no-output behaviour. Ignored when `logger` is passed.
|
|
107
|
+
*/
|
|
108
|
+
logLevel?: ServerLogLevel;
|
|
28
109
|
}
|
|
29
110
|
interface EventOptions {
|
|
30
111
|
/** Ms between automatic event flushes. Default: 2000. */
|
|
31
112
|
flushInterval?: number;
|
|
32
113
|
/** Max events per HTTP request. Default: 10. */
|
|
33
114
|
batchSize?: number;
|
|
34
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* Max events held in a queue. Oldest dropped when exceeded, with a warning.
|
|
117
|
+
* Applies to the tracker's queues AND to the pre-ready queue that holds
|
|
118
|
+
* events tracked before the first datafile arrives. Default: 10_000.
|
|
119
|
+
*/
|
|
35
120
|
maxQueueSize?: number;
|
|
36
121
|
/** Retry attempts on flush failure. Default: 3. */
|
|
37
122
|
retryAttempts?: number;
|
|
38
123
|
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Options for a single evaluation. The CONTEXT is not in here: it is the third
|
|
127
|
+
* positional argument of every evaluation call (breaking change B3), matching
|
|
128
|
+
* `@avsbhq/edge`, so one shape reads the same in every server SDK.
|
|
129
|
+
*/
|
|
130
|
+
interface ServerGetFlagOptions {
|
|
131
|
+
/** Per-call decide options. See ./decideOptions.ts for the two members. */
|
|
132
|
+
decideOptions?: DecideOption[];
|
|
133
|
+
/** Append this evaluation to a decision log. */
|
|
134
|
+
decisionLog?: DecisionLog;
|
|
135
|
+
}
|
|
136
|
+
interface GetAllFlagsOptions {
|
|
137
|
+
/**
|
|
138
|
+
* Record an exposure for every flag evaluated. Default FALSE: reading the
|
|
139
|
+
* whole flag set is a diagnostic or an SSR bootstrap, not a decision a
|
|
140
|
+
* visitor saw, and firing hundreds of exposures per request would corrupt
|
|
141
|
+
* every experiment in the project.
|
|
142
|
+
*/
|
|
143
|
+
fireExposures?: boolean;
|
|
144
|
+
decideOptions?: DecideOption[];
|
|
145
|
+
/** Append every evaluation to a decision log. */
|
|
146
|
+
decisionLog?: DecisionLog;
|
|
147
|
+
}
|
|
39
148
|
type ServerEventMap = {
|
|
40
149
|
ready: () => void;
|
|
150
|
+
/** A newly published datafile was applied, and how it arrived. */
|
|
41
151
|
configUpdate: (data: {
|
|
42
152
|
publishedAt: string;
|
|
153
|
+
reason: 'poll' | 'stream' | 'manual';
|
|
43
154
|
}) => void;
|
|
155
|
+
/** One flag was evaluated. Fires whether or not an exposure was recorded. */
|
|
44
156
|
evaluation: (data: {
|
|
45
157
|
flagKey: string;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
source: EvaluationSource;
|
|
158
|
+
flag: Flag;
|
|
159
|
+
contextKeys: string[];
|
|
49
160
|
}) => void;
|
|
161
|
+
/**
|
|
162
|
+
* An exposure was recorded and queued for delivery. This is the server-side
|
|
163
|
+
* seam for forwarding decisions to your own analytics: it carries the SAME
|
|
164
|
+
* `eventId` the ingestion pipeline receives, so rows reconcile across
|
|
165
|
+
* systems.
|
|
166
|
+
*/
|
|
167
|
+
exposure: (event: ExposureEvent) => void;
|
|
50
168
|
error: (error: Error) => void;
|
|
51
|
-
/**
|
|
169
|
+
/** A sticky assignment references a rule that no longer matches the datafile. */
|
|
52
170
|
stickyOrphan: (data: {
|
|
53
171
|
flagKey: string;
|
|
54
172
|
userId: string;
|
|
@@ -56,12 +174,6 @@ type ServerEventMap = {
|
|
|
56
174
|
}) => void;
|
|
57
175
|
};
|
|
58
176
|
type ServerEventName = keyof ServerEventMap;
|
|
59
|
-
interface MiddlewareConfig {
|
|
60
|
-
/** Extract userId from the request. Return undefined to skip AvsB for this request. */
|
|
61
|
-
userIdFrom: (req: unknown) => string | undefined;
|
|
62
|
-
/** Extract targeting attributes from the request. */
|
|
63
|
-
attributesFrom?: (req: unknown) => Record<string, unknown>;
|
|
64
|
-
}
|
|
65
177
|
|
|
66
178
|
/**
|
|
67
179
|
* Purchase order shape for server-side purchase tracking.
|
|
@@ -100,41 +212,6 @@ interface PurchaseOrder {
|
|
|
100
212
|
}>;
|
|
101
213
|
}
|
|
102
214
|
|
|
103
|
-
/**
|
|
104
|
-
* Server-side datasets read client (e-commerce suite Phase 2).
|
|
105
|
-
*
|
|
106
|
-
* Resolves the live version of a dataset from the SDK's datafile
|
|
107
|
-
* (`datasets` field) and fetches a single key from the edge worker:
|
|
108
|
-
*
|
|
109
|
-
* GET {ingestionHost}/v1/ds/{sdkKey}/{slug}/{versionShortId}/{key}
|
|
110
|
-
*
|
|
111
|
-
* The ingestion host is derived from the datafile's `collectEndpoint`,
|
|
112
|
-
* exactly like the server tracker derives `/v1/collect/*`. The worker's
|
|
113
|
-
* key cache resolves SDK keys as well as snippet keys in the first path
|
|
114
|
-
* segment.
|
|
115
|
-
*
|
|
116
|
-
* Error posture matches flag evaluation: this client NEVER throws.
|
|
117
|
-
* Unknown slug, missing live version, SDK not ready, HTTP errors and
|
|
118
|
-
* network failures all degrade to `{ found: false }`.
|
|
119
|
-
*/
|
|
120
|
-
interface DatasetGetResult {
|
|
121
|
-
found: boolean;
|
|
122
|
-
/** Parsed row for TABLE/FEED datasets; `true` for LIST membership hits. */
|
|
123
|
-
value?: unknown;
|
|
124
|
-
meta: {
|
|
125
|
-
slug: string;
|
|
126
|
-
/** Live version short id used for the lookup; null when unresolvable. */
|
|
127
|
-
version: number | null;
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
interface DatasetsClient {
|
|
131
|
-
/**
|
|
132
|
-
* Look up `key` in the live version of dataset `slug`.
|
|
133
|
-
* Resolves the version from the current datafile and never throws.
|
|
134
|
-
*/
|
|
135
|
-
get(slug: string, key: string): Promise<DatasetGetResult>;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
215
|
/**
|
|
139
216
|
* Server-side recommendations client (e-commerce suite overhaul, contract C3).
|
|
140
217
|
*
|
|
@@ -225,156 +302,289 @@ interface DatafileRecipe {
|
|
|
225
302
|
}
|
|
226
303
|
|
|
227
304
|
/**
|
|
228
|
-
* A per-request
|
|
305
|
+
* A per-request client with a bound `EvalContext`. THE primary server pattern:
|
|
306
|
+
* bind the context once at the edge of the request, then read flags anywhere
|
|
307
|
+
* inside it without threading identity through every signature.
|
|
229
308
|
*
|
|
230
309
|
* Created via `server.forUser(ctx)` or the `server.createUserContext(ctx)`
|
|
231
|
-
* alias.
|
|
310
|
+
* alias. Every flag and track call uses the bound context, and the instance
|
|
311
|
+
* carries its own decision log.
|
|
232
312
|
*
|
|
233
|
-
* Forced decisions set
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
* Spec §5.8.
|
|
313
|
+
* Forced decisions set here live only for the lifetime of this instance; they
|
|
314
|
+
* never touch the shared `AvsbServer` override state, which is what makes them
|
|
315
|
+
* safe in test middleware and per-request debugging.
|
|
238
316
|
*/
|
|
239
317
|
interface UserBoundClient {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
getBoolFlag(flagKey: string, defaultValue: boolean, options?:
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}): Flag<string>;
|
|
249
|
-
getNumberFlag(flagKey: string, defaultValue: number, options?: {
|
|
250
|
-
decideOptions?: DecideOption[];
|
|
251
|
-
}): Flag<number>;
|
|
252
|
-
getJsonFlag<T>(flagKey: string, defaultValue: T, options?: {
|
|
253
|
-
decideOptions?: DecideOption[];
|
|
254
|
-
}): Flag<T>;
|
|
318
|
+
/** Evaluate against the bound context. Returns `Flag<T>`, never a raw value. */
|
|
319
|
+
getFlag<T>(flagKey: string, defaultValue: T, options?: ServerGetFlagOptions): Flag<T>;
|
|
320
|
+
/** Type-checked against the datafile's declared flag type; falls back to the default on a mismatch. */
|
|
321
|
+
getBoolFlag(flagKey: string, defaultValue: boolean, options?: ServerGetFlagOptions): Flag<boolean>;
|
|
322
|
+
getStringFlag(flagKey: string, defaultValue: string, options?: ServerGetFlagOptions): Flag<string>;
|
|
323
|
+
getNumberFlag(flagKey: string, defaultValue: number, options?: ServerGetFlagOptions): Flag<number>;
|
|
324
|
+
getJsonFlag<T>(flagKey: string, defaultValue: T, options?: ServerGetFlagOptions): Flag<T>;
|
|
325
|
+
/** Every flag in the environment for the bound context. Exposures off by default. */
|
|
255
326
|
getAllFlags(options?: GetAllFlagsOptions): Record<string, Flag>;
|
|
327
|
+
/** Queue a metric event attributed to the bound context. */
|
|
256
328
|
track(eventKey: string, payload?: Omit<TrackPayload, 'context'>): void;
|
|
329
|
+
/** Send a purchase attributed to the bound context's bucketing key. */
|
|
330
|
+
trackPurchase(order: PurchaseOrder): Promise<void>;
|
|
331
|
+
/** Record an exposure for a decision the visitor has now actually seen. */
|
|
332
|
+
manualExposure(flagKey: string): void;
|
|
333
|
+
/** The context this client is bound to. */
|
|
334
|
+
getContext(): EvalContext;
|
|
257
335
|
setForcedDecision(flagKey: string, opts: {
|
|
258
336
|
ruleId?: string;
|
|
259
337
|
variationKey: string;
|
|
260
338
|
}): void;
|
|
261
339
|
clearForcedDecision(flagKey: string): void;
|
|
340
|
+
/** Every decision this request made, redacted per the context's private attributes. */
|
|
262
341
|
getDecisionLog(): DecisionLog;
|
|
263
342
|
}
|
|
264
343
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
344
|
+
/**
|
|
345
|
+
* @internal Superset of the public per-call options, carrying the two things
|
|
346
|
+
* only this package may set. Not exported from the package entry.
|
|
347
|
+
*/
|
|
348
|
+
interface InternalGetFlagOptions extends ServerGetFlagOptions {
|
|
349
|
+
/** Forced decision from a UserBoundClient, applied before evaluation. */
|
|
270
350
|
forcedVariationKey?: string;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
fireExposures?: boolean;
|
|
274
|
-
decideOptions?: DecideOption[];
|
|
351
|
+
/** Runtime type check to apply, from a typed getter. */
|
|
352
|
+
expectedType?: 'boolean' | 'string' | 'number' | 'json';
|
|
275
353
|
}
|
|
276
354
|
declare class AvsbServer {
|
|
277
|
-
private sdkKey;
|
|
278
|
-
private
|
|
279
|
-
private
|
|
280
|
-
private initTimeout;
|
|
281
|
-
private logger;
|
|
282
|
-
private stickyBucketService;
|
|
283
|
-
private eventOptions;
|
|
284
|
-
private streamingEnabled;
|
|
285
|
-
private
|
|
355
|
+
private readonly sdkKey;
|
|
356
|
+
private readonly cdnHost;
|
|
357
|
+
private readonly pollingInterval;
|
|
358
|
+
private readonly initTimeout;
|
|
359
|
+
private readonly logger;
|
|
360
|
+
private readonly stickyBucketService;
|
|
361
|
+
private readonly eventOptions;
|
|
362
|
+
private readonly streamingEnabled;
|
|
363
|
+
private readonly streamingEndpointOption;
|
|
364
|
+
private readonly overrides;
|
|
365
|
+
private readonly resolveDecideOptions;
|
|
286
366
|
private datafile;
|
|
367
|
+
private datafileEtag;
|
|
287
368
|
private ready;
|
|
288
369
|
private destroyed;
|
|
289
370
|
private tracker;
|
|
290
371
|
private streamingClient;
|
|
291
|
-
private
|
|
372
|
+
private pollingTimer;
|
|
292
373
|
private abortController;
|
|
293
|
-
private overrides;
|
|
294
374
|
private listeners;
|
|
295
375
|
private readyPromise;
|
|
296
376
|
private pendingEvents;
|
|
377
|
+
private pendingPurchases;
|
|
297
378
|
/**
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
379
|
+
* Exposures produced before the tracker existed. Third member of the
|
|
380
|
+
* pre-ready family alongside `pendingEvents` and `pendingPurchases`; it used
|
|
381
|
+
* to be missing, and exposures were dropped on the floor instead.
|
|
382
|
+
*/
|
|
383
|
+
private pendingExposures;
|
|
384
|
+
/** ms-epoch of the last liveness heartbeat, for the throttle. */
|
|
385
|
+
private lastHeartbeatAt;
|
|
386
|
+
/** Keys of one-time warnings, so a request loop cannot flood a log. */
|
|
387
|
+
private readonly warnedOnce;
|
|
388
|
+
/**
|
|
389
|
+
* `client.utils.*` surface, constructed lazily on first access so the utils
|
|
390
|
+
* package is only imported when actually consumed.
|
|
305
391
|
*/
|
|
306
392
|
private _utils;
|
|
307
393
|
get utils(): AvsbUtils;
|
|
308
394
|
/**
|
|
309
|
-
* Server-side datasets read surface (
|
|
310
|
-
*
|
|
311
|
-
* `client.datasets.get(slug, key)` resolves the live version from the
|
|
312
|
-
* current datafile and never throws; see ./datasets.ts.
|
|
395
|
+
* Server-side datasets read surface. `client.datasets.get(slug, key)`
|
|
396
|
+
* resolves the live version from the current datafile and never throws.
|
|
313
397
|
*/
|
|
314
398
|
private _datasets;
|
|
315
399
|
get datasets(): DatasetsClient;
|
|
316
400
|
/**
|
|
317
|
-
* Server-side recommendations surface (
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
* tracker's queue and revenue attaches via the existing `trackPurchase`.
|
|
401
|
+
* Server-side recommendations surface. `client.recs.get({ recipe, … })`
|
|
402
|
+
* resolves the recipe registry from the current datafile and never throws;
|
|
403
|
+
* impressions and clicks ride the server tracker's queue and revenue
|
|
404
|
+
* attaches via `trackPurchase`.
|
|
322
405
|
*/
|
|
323
406
|
private _recs;
|
|
324
407
|
get recs(): RecsClient;
|
|
325
408
|
constructor(options: AvsbServerOptions);
|
|
409
|
+
/**
|
|
410
|
+
* Await the init attempt. Never rejects; always resolves to an InitResult.
|
|
411
|
+
* Calling it twice returns the same promise.
|
|
412
|
+
*/
|
|
326
413
|
onReady(): Promise<InitResult>;
|
|
414
|
+
/** True when a datafile is loaded and flags are answerable right now. */
|
|
415
|
+
isReady(): boolean;
|
|
327
416
|
private initializeAndReady;
|
|
417
|
+
/** Wiring that runs once a datafile is readable, however it arrived. */
|
|
418
|
+
private onDatafileReadable;
|
|
328
419
|
close(): Promise<void>;
|
|
329
420
|
flush(): Promise<void>;
|
|
421
|
+
/**
|
|
422
|
+
* Fetch the datafile now and apply it if it changed.
|
|
423
|
+
*
|
|
424
|
+
* A 304 is not a problem and is not warned about: it means the datafile in
|
|
425
|
+
* memory IS the published one.
|
|
426
|
+
*/
|
|
330
427
|
refreshDatafile(): Promise<void>;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
428
|
+
/**
|
|
429
|
+
* Adopt a datafile received out of band: a bootstrap blob from
|
|
430
|
+
* `client.utils.hydrateBootstrap`, a file on disk, a KV read at cold start.
|
|
431
|
+
*
|
|
432
|
+
* Marks the SDK ready, drains anything queued before it, and starts polling
|
|
433
|
+
* and streaming exactly as a network arrival would. This used to be a no-op
|
|
434
|
+
* comment inside the utils adapter, which made the documented bootstrap
|
|
435
|
+
* handoff silently do nothing.
|
|
436
|
+
*/
|
|
437
|
+
applyDatafileBootstrap(datafile: FlagDatafile): void;
|
|
438
|
+
/** The datafile currently in memory, or null before init completes. */
|
|
439
|
+
getDatafile(): FlagDatafile | null;
|
|
440
|
+
getFlag<T>(flagKey: string, defaultValue: NoInfer<T> | T, context?: EvalContext, options?: ServerGetFlagOptions): Flag<T>;
|
|
441
|
+
getBoolFlag(flagKey: string, defaultValue: NoInfer<boolean> | boolean, context?: EvalContext, options?: ServerGetFlagOptions): Flag<boolean>;
|
|
442
|
+
getStringFlag(flagKey: string, defaultValue: NoInfer<string> | string, context?: EvalContext, options?: ServerGetFlagOptions): Flag<string>;
|
|
443
|
+
getNumberFlag(flagKey: string, defaultValue: NoInfer<number> | number, context?: EvalContext, options?: ServerGetFlagOptions): Flag<number>;
|
|
444
|
+
getJsonFlag<T>(flagKey: string, defaultValue: NoInfer<T> | T, context?: EvalContext, options?: ServerGetFlagOptions): Flag<T>;
|
|
445
|
+
/**
|
|
446
|
+
* @internal Single evaluation entry point for this package. `UserBoundClient`
|
|
447
|
+
* calls it with a bound context and forced decisions. Not part of the
|
|
448
|
+
* published API: its options bag carries package-internal fields.
|
|
449
|
+
*/
|
|
450
|
+
evaluateInternal<T>(flagKey: string, defaultValue: NoInfer<T> | T, context?: EvalContext, options?: InternalGetFlagOptions): Flag<T>;
|
|
451
|
+
private evaluateAndBuild;
|
|
452
|
+
private checkType;
|
|
453
|
+
/**
|
|
454
|
+
* Every flag in the environment, evaluated against one context.
|
|
455
|
+
*
|
|
456
|
+
* Positional context (B3). Note the deliberate difference from
|
|
457
|
+
* `@avsbhq/browser`, which keeps an options bag (`getAllFlags({context})`)
|
|
458
|
+
* because it is context-bound and the bag disambiguates its optional
|
|
459
|
+
* arguments.
|
|
460
|
+
*/
|
|
461
|
+
getAllFlags(context?: EvalContext, options?: GetAllFlagsOptions): Record<string, Flag>;
|
|
462
|
+
/**
|
|
463
|
+
* Queue a metric (conversion) event. Synchronous: it returns as soon as the
|
|
464
|
+
* event is queued, and the tracker batches it.
|
|
465
|
+
*
|
|
466
|
+
* The context rides in the payload rather than positionally because it is
|
|
467
|
+
* OPTIONAL vocabulary on the shared `TrackPayload` type and required only on
|
|
468
|
+
* the server; see the README for why the two runtimes read differently here.
|
|
469
|
+
*/
|
|
338
470
|
track(eventKey: string, payload: TrackPayload & {
|
|
339
471
|
context: EvalContext;
|
|
340
472
|
}): void;
|
|
473
|
+
/**
|
|
474
|
+
* Send a purchase to `/v1/collect/orders`.
|
|
475
|
+
*
|
|
476
|
+
* Queued before the tracker exists, exactly like `track` (they used to
|
|
477
|
+
* disagree: one queued, the other warned and dropped the order).
|
|
478
|
+
*/
|
|
479
|
+
trackPurchase(visitorId: string, order: PurchaseOrder): Promise<void>;
|
|
480
|
+
/**
|
|
481
|
+
* Mark the moment a signup or login claimed an anonymous session. Same shape
|
|
482
|
+
* and same honesty as the browser client's `alias`.
|
|
483
|
+
*
|
|
484
|
+
* WHAT IT DOES: queues one `avsb_alias` event attributed to the NEW
|
|
485
|
+
* identity's key.
|
|
486
|
+
*
|
|
487
|
+
* WHAT IT DOES NOT DO, precisely:
|
|
488
|
+
* - It does not rewrite sticky assignments or move past exposures. Nothing
|
|
489
|
+
* already decided for either identity changes.
|
|
490
|
+
* - It does not store the PREVIOUS key. The conversion wire
|
|
491
|
+
* (`/v1/collect/metric-batch`) carries no properties column, so a
|
|
492
|
+
* retroactive session join is not available yet.
|
|
493
|
+
*
|
|
494
|
+
* To keep one visitor's bucketing across a login, evaluate with the SAME
|
|
495
|
+
* context key on both sides. See "Sharing identity with the web snippet" in
|
|
496
|
+
* the README for the cross-surface version of the same rule.
|
|
497
|
+
*/
|
|
498
|
+
alias(previousContext: EvalContext, newContext: EvalContext): void;
|
|
499
|
+
/**
|
|
500
|
+
* Record an exposure for a flag without re-serving its value. Use it when
|
|
501
|
+
* the decision was read earlier with exposures suppressed and the visitor has
|
|
502
|
+
* now actually seen the result.
|
|
503
|
+
*/
|
|
341
504
|
manualExposure(flagKey: string, context: EvalContext): void;
|
|
505
|
+
/** A client with the context bound. The primary server pattern. */
|
|
342
506
|
forUser(context: EvalContext): UserBoundClient;
|
|
507
|
+
/** Ergonomic alias for `forUser`. */
|
|
343
508
|
createUserContext(context: EvalContext): UserBoundClient;
|
|
344
509
|
setOverrideForUser(flagKey: string, contextKey: string, variationKey: string, contextKind?: string): void;
|
|
345
510
|
setGlobalOverride(flagKey: string, variationKey: string): void;
|
|
346
511
|
clearOverrideForUser(flagKey: string, contextKey: string, contextKind?: string): void;
|
|
347
512
|
clearGlobalOverride(flagKey: string): void;
|
|
348
513
|
clearAllOverrides(): void;
|
|
514
|
+
/** A fresh decision log to pass per call. `forUser` carries its own. */
|
|
349
515
|
createDecisionLog(): DecisionLog;
|
|
350
516
|
on<E extends ServerEventName>(event: E, cb: ServerEventMap[E]): () => void;
|
|
351
517
|
off<E extends ServerEventName>(event: E, cb: ServerEventMap[E]): void;
|
|
518
|
+
/**
|
|
519
|
+
* `on` for the shared `ClientEventMap` names the utils layer speaks. The
|
|
520
|
+
* server emits every one of them except `flagChange`, which needs per-flag
|
|
521
|
+
* diffing the server does not do; subscribing to it warns and hands back a
|
|
522
|
+
* no-op disposer instead of pretending.
|
|
523
|
+
*/
|
|
524
|
+
private onSharedEvent;
|
|
525
|
+
private emit;
|
|
526
|
+
private warnOnce;
|
|
352
527
|
private evaluate;
|
|
353
|
-
|
|
528
|
+
/**
|
|
529
|
+
* One exposure, delivered everywhere: queued for ingestion AND emitted on
|
|
530
|
+
* the event bus with the SAME `eventId`, so a customer forwarding decisions
|
|
531
|
+
* to their own analytics can reconcile rows with A vs B results.
|
|
532
|
+
*/
|
|
533
|
+
private deliverExposure;
|
|
354
534
|
private toFlag;
|
|
355
|
-
/**
|
|
356
|
-
private
|
|
357
|
-
/**
|
|
358
|
-
private
|
|
535
|
+
/** Build a Flag from a forced variation key (per-request forced decision). */
|
|
536
|
+
private buildForcedFlag;
|
|
537
|
+
/** Emit the evaluation event and append to a decision log if one was passed. */
|
|
538
|
+
private recordDecision;
|
|
359
539
|
private buildLegacyContext;
|
|
360
|
-
private resolveRuntimeOverride;
|
|
361
540
|
private resolveStickyBucket;
|
|
362
541
|
private findVariation;
|
|
363
542
|
private fireExposureForStickyAssignment;
|
|
364
|
-
private emit;
|
|
365
|
-
private emitEvaluation;
|
|
366
543
|
private initTracker;
|
|
367
|
-
private
|
|
368
|
-
private
|
|
544
|
+
private enqueuePendingExposure;
|
|
545
|
+
private enqueuePending;
|
|
546
|
+
/**
|
|
547
|
+
* Fetch and apply the datafile.
|
|
548
|
+
*
|
|
549
|
+
* Three outcomes, not two, because a conditional request has three:
|
|
550
|
+
* 'applied' a datafile came back and is now in memory
|
|
551
|
+
* 'unchanged' the CDN answered 304: what we hold IS current
|
|
552
|
+
* 'failed' nothing usable came back; the error is already logged
|
|
553
|
+
*
|
|
554
|
+
* 'unchanged' is a success. Collapsing it into 'failed' made
|
|
555
|
+
* `refreshDatafile()` warn that a refresh "did not apply a new datafile"
|
|
556
|
+
* every time the datafile was simply up to date.
|
|
557
|
+
*/
|
|
558
|
+
private loadDatafile;
|
|
559
|
+
/**
|
|
560
|
+
* Poll on a JITTERED interval. Without the jitter every process that
|
|
561
|
+
* deployed together polls the CDN on the same tick forever.
|
|
562
|
+
*/
|
|
369
563
|
private startPolling;
|
|
564
|
+
/**
|
|
565
|
+
* Open the SSE stream when one is available, and fall back to polling in
|
|
566
|
+
* silence when it is not (B12). The endpoint comes from the datafile's
|
|
567
|
+
* `streamEndpoint` field, or an explicit option; it is never guessed from
|
|
568
|
+
* another host, which is what gave one port an infinite reconnect loop
|
|
569
|
+
* against DNS that does not resolve.
|
|
570
|
+
*/
|
|
571
|
+
private startStreamingIfEnabled;
|
|
572
|
+
/**
|
|
573
|
+
* Report liveness, throttled.
|
|
574
|
+
*
|
|
575
|
+
* Called on every successful datafile load, not just at init: the dashboard
|
|
576
|
+
* marks an environment Stale after five minutes without a heartbeat, so an
|
|
577
|
+
* init-only heartbeat made every long-running process look dead a few
|
|
578
|
+
* minutes after it started. The throttle keeps a fast polling interval (or a
|
|
579
|
+
* burst of stream-triggered refreshes) from turning liveness into traffic.
|
|
580
|
+
*
|
|
581
|
+
* Fire and forget: a failed heartbeat never affects evaluation, and the
|
|
582
|
+
* throttle clock is stamped by the synchronous send below, so a hanging
|
|
583
|
+
* request cannot queue a second one.
|
|
584
|
+
*/
|
|
370
585
|
private sendHeartbeat;
|
|
371
586
|
}
|
|
372
587
|
|
|
373
|
-
declare function expressMiddleware(client: AvsbServer, config: MiddlewareConfig): (req: unknown, res: unknown, next: () => void) => void;
|
|
374
|
-
declare function fastifyPlugin(instance: unknown, opts: {
|
|
375
|
-
client: AvsbServer;
|
|
376
|
-
} & MiddlewareConfig, done: () => void): void;
|
|
377
|
-
|
|
378
588
|
declare class InMemoryStickyBucketService implements StickyBucketService {
|
|
379
589
|
private cache;
|
|
380
590
|
private key;
|
|
@@ -412,6 +622,60 @@ declare class RedisStickyBucketService implements StickyBucketService {
|
|
|
412
622
|
|
|
413
623
|
declare function createDecisionLog(): DecisionLog;
|
|
414
624
|
|
|
415
|
-
|
|
625
|
+
/** Default replay window: a delivery more than 5 minutes old is rejected. */
|
|
626
|
+
declare const DEFAULT_TOLERANCE_SECONDS = 300;
|
|
627
|
+
/**
|
|
628
|
+
* Header bag shapes this accepts, so it works with Node's `IncomingHttpHeaders`,
|
|
629
|
+
* a plain object, or a `Headers` instance (via `Object.fromEntries(headers)`).
|
|
630
|
+
*/
|
|
631
|
+
type WebhookHeaders = Record<string, string | string[] | undefined> | {
|
|
632
|
+
get(name: string): string | null;
|
|
633
|
+
};
|
|
634
|
+
interface VerifyWebhookOptions {
|
|
635
|
+
/**
|
|
636
|
+
* How old a delivery may be, in seconds. Defaults to 300 (5 minutes). Set to 0 to
|
|
637
|
+
* skip the age check entirely — only do that when you dedupe on the delivery id and
|
|
638
|
+
* accept replays.
|
|
639
|
+
*/
|
|
640
|
+
toleranceSeconds?: number;
|
|
641
|
+
/** Injectable clock, in ms since the epoch. Exists for tests. */
|
|
642
|
+
now?: () => number;
|
|
643
|
+
}
|
|
644
|
+
/** Why verification failed. `ok: false` always carries one of these. */
|
|
645
|
+
type WebhookVerifyFailure = 'missing_headers' | 'malformed_signature' | 'malformed_timestamp' | 'timestamp_out_of_tolerance' | 'signature_mismatch';
|
|
646
|
+
type WebhookVerifyResult = {
|
|
647
|
+
ok: true;
|
|
648
|
+
deliveryId: string;
|
|
649
|
+
timestamp: string;
|
|
650
|
+
} | {
|
|
651
|
+
ok: false;
|
|
652
|
+
reason: WebhookVerifyFailure;
|
|
653
|
+
message: string;
|
|
654
|
+
};
|
|
655
|
+
/**
|
|
656
|
+
* Verify one webhook delivery.
|
|
657
|
+
*
|
|
658
|
+
* @param payload The RAW request body, exactly as received (string or Buffer). Never
|
|
659
|
+
* a parsed-and-re-serialised object.
|
|
660
|
+
* @param headers The request headers.
|
|
661
|
+
* @param secret The webhook's signing secret, from the create or rotate response.
|
|
662
|
+
*
|
|
663
|
+
* @example
|
|
664
|
+
* ```ts
|
|
665
|
+
* import express from 'express'
|
|
666
|
+
* import { verifyWebhookSignature } from '@avsbhq/node'
|
|
667
|
+
*
|
|
668
|
+
* app.post('/avsb-webhook', express.raw({ type: 'application/json' }), (req, res) => {
|
|
669
|
+
* const result = verifyWebhookSignature(req.body, req.headers, process.env.AVSB_WEBHOOK_SECRET!)
|
|
670
|
+
* if (!result.ok) return res.status(401).send(result.reason)
|
|
671
|
+
* const event = JSON.parse(req.body.toString('utf8'))
|
|
672
|
+
* // ... handle event, deduped on result.deliveryId
|
|
673
|
+
* res.sendStatus(200)
|
|
674
|
+
* })
|
|
675
|
+
* ```
|
|
676
|
+
*/
|
|
677
|
+
declare function verifyWebhookSignature(payload: string | Buffer, headers: WebhookHeaders, secret: string, options?: VerifyWebhookOptions): WebhookVerifyResult;
|
|
678
|
+
|
|
679
|
+
declare const SDK_VERSION = "1.7.1";
|
|
416
680
|
|
|
417
|
-
export { AvsbServer, type AvsbServerOptions, type DatafileRecipe, type DatasetGetResult, type DatasetsClient, type EventOptions, type GetAllFlagsOptions,
|
|
681
|
+
export { AvsbServer, type AvsbServerOptions, DEFAULT_TOLERANCE_SECONDS, type DatafileRecipe, type DatasetGetResult, type DatasetsClient, type EventOptions, type GetAllFlagsOptions, InMemoryStickyBucketService, type PurchaseOrder, type RecProduct, type RecsAttribution, type RecsClient, type RecsGetRequest, type RecsResult, type RedisLike, type RedisStickyBucketOptions, RedisStickyBucketService, SDK_VERSION, type ServerEventMap, type ServerEventName, type ServerGetFlagOptions, type ServerLogLevel, type UserBoundClient, type VerifyWebhookOptions, type WebhookHeaders, type WebhookVerifyFailure, type WebhookVerifyResult, createDecisionLog, defaultLogger, isDevEnvironment, resolveServerLogger, verifyWebhookSignature };
|