@avsbhq/node 1.5.0 → 1.6.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/README.md +14 -14
- package/dist/index.cjs +11 -11
- package/dist/index.d.cts +11 -7
- package/dist/index.d.ts +11 -7
- package/dist/index.js +11 -11
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ Get the SDK key for the environment you want to target from your A vs B project'
|
|
|
55
55
|
AVSB_SDK_KEY=sdk_production_...
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
When used server-side, the SDK key only ever needs to leave the server to hit the datafile CDN
|
|
58
|
+
When used server-side, the SDK key only ever needs to leave the server to hit the datafile CDN; it should never end up in client-rendered HTML, browser bundles, or browser-accessible config. Store it in environment variables only.
|
|
59
59
|
|
|
60
60
|
Server SDK keys cannot be rotated without a process restart; the new key must be set in the environment before the server boots (or trigger a rolling restart in your orchestrator).
|
|
61
61
|
|
|
@@ -63,7 +63,7 @@ Server SDK keys cannot be rotated without a process restart; the new key must be
|
|
|
63
63
|
|
|
64
64
|
## 4. Identity
|
|
65
65
|
|
|
66
|
-
The server SDK is stateless per request
|
|
66
|
+
The server SDK is stateless per request; it does not hold a persistent context. Pass the user context on each call using `forUser()` or as a `context` option on individual calls.
|
|
67
67
|
|
|
68
68
|
### `forUser(context)` / `createUserContext(context)`
|
|
69
69
|
|
|
@@ -106,7 +106,7 @@ const flag = server.getBoolFlag('big-cart-banner', false, {
|
|
|
106
106
|
viewedSkus: ['SKU-1', 'SKU-2'], // for "products viewed" matching on SKU
|
|
107
107
|
viewedCategories: ['trainers'], // for "products viewed" matching on category
|
|
108
108
|
purchaseHistory: {
|
|
109
|
-
// PurchaseHistoryAttribute
|
|
109
|
+
// PurchaseHistoryAttribute, exported for your own declarations
|
|
110
110
|
hasPurchased: true,
|
|
111
111
|
lastPurchaseAt: 1765500000000, // epoch milliseconds (optional)
|
|
112
112
|
lifetimeSpend: 540.0, // decimal MAJOR units (optional)
|
|
@@ -118,9 +118,9 @@ const flag = server.getBoolFlag('big-cart-banner', false, {
|
|
|
118
118
|
|
|
119
119
|
Server-side semantics to be aware of:
|
|
120
120
|
|
|
121
|
-
- **All money is decimal major units.** Condition values are stored in minor units and converted using the project currency exponent from the datafile (default 2)
|
|
122
|
-
- **Missing or mistyped attributes evaluate the condition to `false`.** A completely absent `purchaseHistory` makes ALL purchase-history operators false
|
|
123
|
-
- **Dataset-backed "products viewed" conditions evaluate `false` on the server.** Dataset view windows live in the browser; server callers use literal SKU/category lists. The session/30-day window distinction is also browser-only
|
|
121
|
+
- **All money is decimal major units.** Condition values are stored in minor units and converted using the project currency exponent from the datafile (default 2); you never pass minor units.
|
|
122
|
+
- **Missing or mistyped attributes evaluate the condition to `false`.** A completely absent `purchaseHistory` makes ALL purchase-history operators false, including `has_not_purchased`. On the server, absence means "not provided", not "never purchased" (this deliberately diverges from the browser snippet, where a profile miss is authoritative).
|
|
123
|
+
- **Dataset-backed "products viewed" conditions evaluate `false` on the server.** Dataset view windows live in the browser; server callers use literal SKU/category lists. The session/30-day window distinction is also browser-only; supply whatever view list you consider current.
|
|
124
124
|
|
|
125
125
|
### `alias(previousContext, newContext)`
|
|
126
126
|
|
|
@@ -135,7 +135,7 @@ await server.alias(
|
|
|
135
135
|
|
|
136
136
|
### Sharing identity with the web snippet
|
|
137
137
|
|
|
138
|
-
To measure one metric across both the A vs B web snippet and this SDK (the setup behind cross-project metrics), the context `key` must equal the browser snippet's visitor id
|
|
138
|
+
To measure one metric across both the A vs B web snippet and this SDK (the setup behind cross-project metrics), the context `key` must equal the browser snippet's visitor id: the `_avsb_visitor` cookie UUID returned by `avsb.getVisitorId()`. Results join on raw id equality, so a different key means the two surfaces never line up. Forward the snippet id from the browser and use it as the key here, or link an internal account id to it with `alias()`. Full guide: [Shared Visitor Identity](https://avsb.cloud/docs/developer-reference/shared-visitor-identity).
|
|
139
139
|
|
|
140
140
|
---
|
|
141
141
|
|
|
@@ -180,13 +180,13 @@ const flag = server.getFlag<boolean>('checkout-v2', false, {
|
|
|
180
180
|
### The `Flag<T>` object
|
|
181
181
|
|
|
182
182
|
```ts
|
|
183
|
-
flag.value // T
|
|
183
|
+
flag.value // T, the variation value
|
|
184
184
|
flag.isEnabled() // true if source === 'rule' && value is truthy
|
|
185
185
|
flag.variationKey // 'on' | 'off' | 'control' | null (null = default served)
|
|
186
186
|
flag.exists() // false when source === 'not_found'
|
|
187
187
|
flag.source // 'rule' | 'sticky' | 'holdout' | 'datafileOverride' | 'default' | ...
|
|
188
188
|
flag.ruleId // which rule matched
|
|
189
|
-
flag.reasons // string[]
|
|
189
|
+
flag.reasons // string[], evaluation path
|
|
190
190
|
```
|
|
191
191
|
|
|
192
192
|
### `getAllFlags(context, options?)`
|
|
@@ -275,7 +275,7 @@ if (!result.success && !result.degraded) {
|
|
|
275
275
|
}
|
|
276
276
|
```
|
|
277
277
|
|
|
278
|
-
`onReady()` never rejects
|
|
278
|
+
`onReady()` never rejects; it always resolves. Check `result.success` to decide whether to proceed.
|
|
279
279
|
|
|
280
280
|
### Event bus
|
|
281
281
|
|
|
@@ -382,7 +382,7 @@ app.get('/checkout', (req, res) => {
|
|
|
382
382
|
})
|
|
383
383
|
```
|
|
384
384
|
|
|
385
|
-
`expressMiddleware` decorates each request with `req.avsb
|
|
385
|
+
`expressMiddleware` decorates each request with `req.avsb`: a `UserBoundClient` scoped to the context returned by `contextFrom`. It also opens an `AsyncLocalStorage` scope so `getRequestClient()` from `@avsbhq/utils` works anywhere in the call chain.
|
|
386
386
|
|
|
387
387
|
### Fastify
|
|
388
388
|
|
|
@@ -443,7 +443,7 @@ DynamoDB and PostgreSQL adapters are available in `@avsbhq/utils`.
|
|
|
443
443
|
|
|
444
444
|
### Real-time streaming
|
|
445
445
|
|
|
446
|
-
Enable SSE streaming to receive datafile updates the instant a flag changes
|
|
446
|
+
Enable SSE streaming to receive datafile updates the instant a flag changes, with no polling delay:
|
|
447
447
|
|
|
448
448
|
```ts
|
|
449
449
|
const server = new AvsbServer({
|
|
@@ -480,7 +480,7 @@ const entries = user.getDecisionLog().entries
|
|
|
480
480
|
| `client.variation('key', context, default)` | `server.forUser(ctx).getBoolFlag('key', default).value` |
|
|
481
481
|
| `client.variationDetail('key', context, default)` | `server.forUser(ctx).getFlag('key', default)` |
|
|
482
482
|
| `client.track('event', context, { metricValue })` | `server.forUser(ctx).track('event', { value })` |
|
|
483
|
-
| `client.identify(context)` | not applicable
|
|
483
|
+
| `client.identify(context)` | not applicable (server is stateless); use `forUser(ctx)` |
|
|
484
484
|
| `client.allFlagsState(context)` | `server.getAllFlags(ctx)` |
|
|
485
485
|
| `client.close()` | `await server.close()` |
|
|
486
486
|
|
|
@@ -488,7 +488,7 @@ Key differences:
|
|
|
488
488
|
- `getFlag` returns `Flag<T>` not a raw value. Access `.value` for the primitive.
|
|
489
489
|
- `track` uses `value` (not `metricValue`) for the numeric metric.
|
|
490
490
|
- The server is context-free at construction; all evaluation is per-request via `forUser()`.
|
|
491
|
-
- Multi-context is native
|
|
491
|
+
- Multi-context is native, with no `ldUser` adapter needed.
|
|
492
492
|
|
|
493
493
|
### From Statsig Node
|
|
494
494
|
|
package/dist/index.cjs
CHANGED
|
@@ -50,7 +50,7 @@ function createDatasetsClient(options) {
|
|
|
50
50
|
const requestTimeout = options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
|
|
51
51
|
async function get(slug, key) {
|
|
52
52
|
if (!slug || !key) {
|
|
53
|
-
logger.warn("datasets.get called with an empty slug or key
|
|
53
|
+
logger.warn("datasets.get called with an empty slug or key; returning not found");
|
|
54
54
|
return miss(slug, null);
|
|
55
55
|
}
|
|
56
56
|
const datafile = getDatafile();
|
|
@@ -60,11 +60,11 @@ function createDatasetsClient(options) {
|
|
|
60
60
|
}
|
|
61
61
|
const dataset = datafile.datasets?.find((d) => d.slug === slug);
|
|
62
62
|
if (!dataset) {
|
|
63
|
-
logger.debug(`Dataset "${slug}" not present in datafile
|
|
63
|
+
logger.debug(`Dataset "${slug}" not present in datafile; returning not found`);
|
|
64
64
|
return miss(slug, null);
|
|
65
65
|
}
|
|
66
66
|
if (!dataset.liveVersion) {
|
|
67
|
-
logger.debug(`Dataset "${slug}" has no live version
|
|
67
|
+
logger.debug(`Dataset "${slug}" has no live version; returning not found`);
|
|
68
68
|
return miss(slug, null);
|
|
69
69
|
}
|
|
70
70
|
const base = datafile.collectEndpoint.replace(/\/+$/, "");
|
|
@@ -172,7 +172,7 @@ function createRecsClient(options) {
|
|
|
172
172
|
try {
|
|
173
173
|
const recipeHandle = typeof req?.recipe === "string" ? req.recipe : "";
|
|
174
174
|
if (recipeHandle === "") {
|
|
175
|
-
logger.warn("recs.get called with an empty recipe
|
|
175
|
+
logger.warn("recs.get called with an empty recipe; returning served:false");
|
|
176
176
|
return miss2("", false);
|
|
177
177
|
}
|
|
178
178
|
const datafile = getDatafile();
|
|
@@ -183,7 +183,7 @@ function createRecsClient(options) {
|
|
|
183
183
|
const recipes = datafile.recipes;
|
|
184
184
|
const entry = recipes?.find((r) => r.recipe === recipeHandle) ?? null;
|
|
185
185
|
if (!entry) {
|
|
186
|
-
logger.debug(`Recipe "${recipeHandle}" not present in datafile
|
|
186
|
+
logger.debug(`Recipe "${recipeHandle}" not present in datafile; returning served:false`);
|
|
187
187
|
return miss2(recipeHandle, false);
|
|
188
188
|
}
|
|
189
189
|
const seeds = resolveSeeds(req.context);
|
|
@@ -260,11 +260,11 @@ function createRecsClient(options) {
|
|
|
260
260
|
}
|
|
261
261
|
function trackClick(productId, opts) {
|
|
262
262
|
if (typeof productId !== "string" || productId === "") {
|
|
263
|
-
logger.warn("recs.trackClick called with an empty productId
|
|
263
|
+
logger.warn("recs.trackClick called with an empty productId; dropped");
|
|
264
264
|
return;
|
|
265
265
|
}
|
|
266
266
|
if (!opts || !opts.visitorId || !opts.experimentId || !opts.variationId) {
|
|
267
|
-
logger.warn("recs.trackClick requires visitorId/experimentId/variationId
|
|
267
|
+
logger.warn("recs.trackClick requires visitorId/experimentId/variationId; dropped");
|
|
268
268
|
return;
|
|
269
269
|
}
|
|
270
270
|
const tracker = getTracker();
|
|
@@ -637,7 +637,7 @@ var AvsbServer = class {
|
|
|
637
637
|
readyPromise = null;
|
|
638
638
|
pendingEvents = [];
|
|
639
639
|
/**
|
|
640
|
-
* §6
|
|
640
|
+
* §6: `client.utils.*` surface. Constructed lazily on first access so
|
|
641
641
|
* the utils package is only imported when actually consumed.
|
|
642
642
|
*
|
|
643
643
|
* The server's event surface (`ServerEventMap`) is a subset of the
|
|
@@ -670,7 +670,7 @@ var AvsbServer = class {
|
|
|
670
670
|
* Server-side datasets read surface (e-commerce suite Phase 2).
|
|
671
671
|
* Constructed lazily on first access, mirroring the `utils` getter.
|
|
672
672
|
* `client.datasets.get(slug, key)` resolves the live version from the
|
|
673
|
-
* current datafile and never throws
|
|
673
|
+
* current datafile and never throws; see ./datasets.ts.
|
|
674
674
|
*/
|
|
675
675
|
_datasets = null;
|
|
676
676
|
get datasets() {
|
|
@@ -824,7 +824,7 @@ var AvsbServer = class {
|
|
|
824
824
|
const decisionLog = options?.decisionLog;
|
|
825
825
|
if (!context) {
|
|
826
826
|
this.logger.warn(
|
|
827
|
-
`getFlag called without context for flagKey="${flagKey}"
|
|
827
|
+
`getFlag called without context for flagKey="${flagKey}"; returning default`
|
|
828
828
|
);
|
|
829
829
|
return (0, import_core2.notFoundFlag)(defaultValue, "no context provided");
|
|
830
830
|
}
|
|
@@ -1086,7 +1086,7 @@ var AvsbServer = class {
|
|
|
1086
1086
|
const mismatch = (0, import_core2.checkExpectedType)(datafileFlag, expectedType, flag.value);
|
|
1087
1087
|
if (mismatch) {
|
|
1088
1088
|
this.logger.warn(
|
|
1089
|
-
`Type mismatch on flag "${flagKey}": ${mismatch}
|
|
1089
|
+
`Type mismatch on flag "${flagKey}": ${mismatch}; returning default`
|
|
1090
1090
|
);
|
|
1091
1091
|
return (0, import_core2.notFoundFlag)(defaultValue, mismatch);
|
|
1092
1092
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -66,7 +66,7 @@ interface MiddlewareConfig {
|
|
|
66
66
|
/**
|
|
67
67
|
* Purchase order shape for server-side purchase tracking.
|
|
68
68
|
* Mirrors the client-side snippet PurchaseOrder interface.
|
|
69
|
-
* Decimal values are forwarded as-is
|
|
69
|
+
* Decimal values are forwarded as-is; no server-side currency conversion.
|
|
70
70
|
*/
|
|
71
71
|
interface PurchaseOrder {
|
|
72
72
|
/** Required. Unique order/transaction identifier. */
|
|
@@ -80,6 +80,10 @@ interface PurchaseOrder {
|
|
|
80
80
|
tax?: number;
|
|
81
81
|
discount?: number;
|
|
82
82
|
coupon?: string;
|
|
83
|
+
/** Mark this as a test purchase. Test orders are stored but excluded from all
|
|
84
|
+
* revenue, profit, recommendation, and audience calculations. Only send when
|
|
85
|
+
* intentionally testing your integration. */
|
|
86
|
+
test?: boolean;
|
|
83
87
|
items?: Array<{
|
|
84
88
|
sku: string;
|
|
85
89
|
/**
|
|
@@ -145,7 +149,7 @@ interface DatasetsClient {
|
|
|
145
149
|
* key field simply carries the SDK key.
|
|
146
150
|
*
|
|
147
151
|
* Attribution is OPT-IN server-side: `get()` fires a `rec:impression` (via
|
|
148
|
-
* the server tracker) only when the caller passes `attribution
|
|
152
|
+
* the server tracker) only when the caller passes `attribution`; a server
|
|
149
153
|
* has no ambient variation context the way a browser harness does.
|
|
150
154
|
*
|
|
151
155
|
* Error posture matches datasets/flag evaluation: this client NEVER throws.
|
|
@@ -198,7 +202,7 @@ interface RecsClient {
|
|
|
198
202
|
get(req: RecsGetRequest): Promise<RecsResult>;
|
|
199
203
|
/**
|
|
200
204
|
* Queue a rec:click for a product the shopper clicked. Pass `recipe` when
|
|
201
|
-
* known
|
|
205
|
+
* known: results group click-through by (variation, recipe).
|
|
202
206
|
*/
|
|
203
207
|
trackClick(productId: string, opts: RecsAttribution & {
|
|
204
208
|
position?: number;
|
|
@@ -207,7 +211,7 @@ interface RecsClient {
|
|
|
207
211
|
}
|
|
208
212
|
/**
|
|
209
213
|
* One entry of the datafile recipe registry (contract C3). Additive on
|
|
210
|
-
* FlagDatafile
|
|
214
|
+
* FlagDatafile; SDKs on older datafiles see it absent.
|
|
211
215
|
*/
|
|
212
216
|
interface DatafileRecipe {
|
|
213
217
|
recipe: string;
|
|
@@ -227,7 +231,7 @@ interface DatafileRecipe {
|
|
|
227
231
|
* alias. All flag evaluations and track calls use the bound context.
|
|
228
232
|
*
|
|
229
233
|
* Forced decisions set on a UserBoundClient survive only for the lifetime
|
|
230
|
-
* of the instance
|
|
234
|
+
* of the instance; they do not affect the shared AvsbServer override state.
|
|
231
235
|
* This makes them safe for test middleware and per-request debugging.
|
|
232
236
|
*
|
|
233
237
|
* Spec §5.8.
|
|
@@ -291,7 +295,7 @@ declare class AvsbServer {
|
|
|
291
295
|
private readyPromise;
|
|
292
296
|
private pendingEvents;
|
|
293
297
|
/**
|
|
294
|
-
* §6
|
|
298
|
+
* §6: `client.utils.*` surface. Constructed lazily on first access so
|
|
295
299
|
* the utils package is only imported when actually consumed.
|
|
296
300
|
*
|
|
297
301
|
* The server's event surface (`ServerEventMap`) is a subset of the
|
|
@@ -305,7 +309,7 @@ declare class AvsbServer {
|
|
|
305
309
|
* Server-side datasets read surface (e-commerce suite Phase 2).
|
|
306
310
|
* Constructed lazily on first access, mirroring the `utils` getter.
|
|
307
311
|
* `client.datasets.get(slug, key)` resolves the live version from the
|
|
308
|
-
* current datafile and never throws
|
|
312
|
+
* current datafile and never throws; see ./datasets.ts.
|
|
309
313
|
*/
|
|
310
314
|
private _datasets;
|
|
311
315
|
get datasets(): DatasetsClient;
|
package/dist/index.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ interface MiddlewareConfig {
|
|
|
66
66
|
/**
|
|
67
67
|
* Purchase order shape for server-side purchase tracking.
|
|
68
68
|
* Mirrors the client-side snippet PurchaseOrder interface.
|
|
69
|
-
* Decimal values are forwarded as-is
|
|
69
|
+
* Decimal values are forwarded as-is; no server-side currency conversion.
|
|
70
70
|
*/
|
|
71
71
|
interface PurchaseOrder {
|
|
72
72
|
/** Required. Unique order/transaction identifier. */
|
|
@@ -80,6 +80,10 @@ interface PurchaseOrder {
|
|
|
80
80
|
tax?: number;
|
|
81
81
|
discount?: number;
|
|
82
82
|
coupon?: string;
|
|
83
|
+
/** Mark this as a test purchase. Test orders are stored but excluded from all
|
|
84
|
+
* revenue, profit, recommendation, and audience calculations. Only send when
|
|
85
|
+
* intentionally testing your integration. */
|
|
86
|
+
test?: boolean;
|
|
83
87
|
items?: Array<{
|
|
84
88
|
sku: string;
|
|
85
89
|
/**
|
|
@@ -145,7 +149,7 @@ interface DatasetsClient {
|
|
|
145
149
|
* key field simply carries the SDK key.
|
|
146
150
|
*
|
|
147
151
|
* Attribution is OPT-IN server-side: `get()` fires a `rec:impression` (via
|
|
148
|
-
* the server tracker) only when the caller passes `attribution
|
|
152
|
+
* the server tracker) only when the caller passes `attribution`; a server
|
|
149
153
|
* has no ambient variation context the way a browser harness does.
|
|
150
154
|
*
|
|
151
155
|
* Error posture matches datasets/flag evaluation: this client NEVER throws.
|
|
@@ -198,7 +202,7 @@ interface RecsClient {
|
|
|
198
202
|
get(req: RecsGetRequest): Promise<RecsResult>;
|
|
199
203
|
/**
|
|
200
204
|
* Queue a rec:click for a product the shopper clicked. Pass `recipe` when
|
|
201
|
-
* known
|
|
205
|
+
* known: results group click-through by (variation, recipe).
|
|
202
206
|
*/
|
|
203
207
|
trackClick(productId: string, opts: RecsAttribution & {
|
|
204
208
|
position?: number;
|
|
@@ -207,7 +211,7 @@ interface RecsClient {
|
|
|
207
211
|
}
|
|
208
212
|
/**
|
|
209
213
|
* One entry of the datafile recipe registry (contract C3). Additive on
|
|
210
|
-
* FlagDatafile
|
|
214
|
+
* FlagDatafile; SDKs on older datafiles see it absent.
|
|
211
215
|
*/
|
|
212
216
|
interface DatafileRecipe {
|
|
213
217
|
recipe: string;
|
|
@@ -227,7 +231,7 @@ interface DatafileRecipe {
|
|
|
227
231
|
* alias. All flag evaluations and track calls use the bound context.
|
|
228
232
|
*
|
|
229
233
|
* Forced decisions set on a UserBoundClient survive only for the lifetime
|
|
230
|
-
* of the instance
|
|
234
|
+
* of the instance; they do not affect the shared AvsbServer override state.
|
|
231
235
|
* This makes them safe for test middleware and per-request debugging.
|
|
232
236
|
*
|
|
233
237
|
* Spec §5.8.
|
|
@@ -291,7 +295,7 @@ declare class AvsbServer {
|
|
|
291
295
|
private readyPromise;
|
|
292
296
|
private pendingEvents;
|
|
293
297
|
/**
|
|
294
|
-
* §6
|
|
298
|
+
* §6: `client.utils.*` surface. Constructed lazily on first access so
|
|
295
299
|
* the utils package is only imported when actually consumed.
|
|
296
300
|
*
|
|
297
301
|
* The server's event surface (`ServerEventMap`) is a subset of the
|
|
@@ -305,7 +309,7 @@ declare class AvsbServer {
|
|
|
305
309
|
* Server-side datasets read surface (e-commerce suite Phase 2).
|
|
306
310
|
* Constructed lazily on first access, mirroring the `utils` getter.
|
|
307
311
|
* `client.datasets.get(slug, key)` resolves the live version from the
|
|
308
|
-
* current datafile and never throws
|
|
312
|
+
* current datafile and never throws; see ./datasets.ts.
|
|
309
313
|
*/
|
|
310
314
|
private _datasets;
|
|
311
315
|
get datasets(): DatasetsClient;
|
package/dist/index.js
CHANGED
|
@@ -23,7 +23,7 @@ function createDatasetsClient(options) {
|
|
|
23
23
|
const requestTimeout = options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
|
|
24
24
|
async function get(slug, key) {
|
|
25
25
|
if (!slug || !key) {
|
|
26
|
-
logger.warn("datasets.get called with an empty slug or key
|
|
26
|
+
logger.warn("datasets.get called with an empty slug or key; returning not found");
|
|
27
27
|
return miss(slug, null);
|
|
28
28
|
}
|
|
29
29
|
const datafile = getDatafile();
|
|
@@ -33,11 +33,11 @@ function createDatasetsClient(options) {
|
|
|
33
33
|
}
|
|
34
34
|
const dataset = datafile.datasets?.find((d) => d.slug === slug);
|
|
35
35
|
if (!dataset) {
|
|
36
|
-
logger.debug(`Dataset "${slug}" not present in datafile
|
|
36
|
+
logger.debug(`Dataset "${slug}" not present in datafile; returning not found`);
|
|
37
37
|
return miss(slug, null);
|
|
38
38
|
}
|
|
39
39
|
if (!dataset.liveVersion) {
|
|
40
|
-
logger.debug(`Dataset "${slug}" has no live version
|
|
40
|
+
logger.debug(`Dataset "${slug}" has no live version; returning not found`);
|
|
41
41
|
return miss(slug, null);
|
|
42
42
|
}
|
|
43
43
|
const base = datafile.collectEndpoint.replace(/\/+$/, "");
|
|
@@ -145,7 +145,7 @@ function createRecsClient(options) {
|
|
|
145
145
|
try {
|
|
146
146
|
const recipeHandle = typeof req?.recipe === "string" ? req.recipe : "";
|
|
147
147
|
if (recipeHandle === "") {
|
|
148
|
-
logger.warn("recs.get called with an empty recipe
|
|
148
|
+
logger.warn("recs.get called with an empty recipe; returning served:false");
|
|
149
149
|
return miss2("", false);
|
|
150
150
|
}
|
|
151
151
|
const datafile = getDatafile();
|
|
@@ -156,7 +156,7 @@ function createRecsClient(options) {
|
|
|
156
156
|
const recipes = datafile.recipes;
|
|
157
157
|
const entry = recipes?.find((r) => r.recipe === recipeHandle) ?? null;
|
|
158
158
|
if (!entry) {
|
|
159
|
-
logger.debug(`Recipe "${recipeHandle}" not present in datafile
|
|
159
|
+
logger.debug(`Recipe "${recipeHandle}" not present in datafile; returning served:false`);
|
|
160
160
|
return miss2(recipeHandle, false);
|
|
161
161
|
}
|
|
162
162
|
const seeds = resolveSeeds(req.context);
|
|
@@ -233,11 +233,11 @@ function createRecsClient(options) {
|
|
|
233
233
|
}
|
|
234
234
|
function trackClick(productId, opts) {
|
|
235
235
|
if (typeof productId !== "string" || productId === "") {
|
|
236
|
-
logger.warn("recs.trackClick called with an empty productId
|
|
236
|
+
logger.warn("recs.trackClick called with an empty productId; dropped");
|
|
237
237
|
return;
|
|
238
238
|
}
|
|
239
239
|
if (!opts || !opts.visitorId || !opts.experimentId || !opts.variationId) {
|
|
240
|
-
logger.warn("recs.trackClick requires visitorId/experimentId/variationId
|
|
240
|
+
logger.warn("recs.trackClick requires visitorId/experimentId/variationId; dropped");
|
|
241
241
|
return;
|
|
242
242
|
}
|
|
243
243
|
const tracker = getTracker();
|
|
@@ -613,7 +613,7 @@ var AvsbServer = class {
|
|
|
613
613
|
readyPromise = null;
|
|
614
614
|
pendingEvents = [];
|
|
615
615
|
/**
|
|
616
|
-
* §6
|
|
616
|
+
* §6: `client.utils.*` surface. Constructed lazily on first access so
|
|
617
617
|
* the utils package is only imported when actually consumed.
|
|
618
618
|
*
|
|
619
619
|
* The server's event surface (`ServerEventMap`) is a subset of the
|
|
@@ -646,7 +646,7 @@ var AvsbServer = class {
|
|
|
646
646
|
* Server-side datasets read surface (e-commerce suite Phase 2).
|
|
647
647
|
* Constructed lazily on first access, mirroring the `utils` getter.
|
|
648
648
|
* `client.datasets.get(slug, key)` resolves the live version from the
|
|
649
|
-
* current datafile and never throws
|
|
649
|
+
* current datafile and never throws; see ./datasets.ts.
|
|
650
650
|
*/
|
|
651
651
|
_datasets = null;
|
|
652
652
|
get datasets() {
|
|
@@ -800,7 +800,7 @@ var AvsbServer = class {
|
|
|
800
800
|
const decisionLog = options?.decisionLog;
|
|
801
801
|
if (!context) {
|
|
802
802
|
this.logger.warn(
|
|
803
|
-
`getFlag called without context for flagKey="${flagKey}"
|
|
803
|
+
`getFlag called without context for flagKey="${flagKey}"; returning default`
|
|
804
804
|
);
|
|
805
805
|
return notFoundFlag2(defaultValue, "no context provided");
|
|
806
806
|
}
|
|
@@ -1062,7 +1062,7 @@ var AvsbServer = class {
|
|
|
1062
1062
|
const mismatch = checkExpectedType(datafileFlag, expectedType, flag.value);
|
|
1063
1063
|
if (mismatch) {
|
|
1064
1064
|
this.logger.warn(
|
|
1065
|
-
`Type mismatch on flag "${flagKey}": ${mismatch}
|
|
1065
|
+
`Type mismatch on flag "${flagKey}": ${mismatch}; returning default`
|
|
1066
1066
|
);
|
|
1067
1067
|
return notFoundFlag2(defaultValue, mismatch);
|
|
1068
1068
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avsbhq/node",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"test:watch": "vitest"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@avsbhq/core": "1.
|
|
37
|
-
"@avsbhq/utils": "1.0.
|
|
36
|
+
"@avsbhq/core": "1.4.0",
|
|
37
|
+
"@avsbhq/utils": "1.0.2"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"tsup": "^8.0.0",
|