@avsbhq/node 1.2.0 → 1.6.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 CHANGED
@@ -93,6 +93,35 @@ const flag = server.getBoolFlag('checkout-v2', false, {
93
93
  })
94
94
  ```
95
95
 
96
+ ### Commerce targeting attributes
97
+
98
+ Audiences built with the commerce condition types (cart value, products viewed, purchase history) evaluate from attributes you supply on the context. The attribute names are fixed:
99
+
100
+ ```ts
101
+ const flag = server.getBoolFlag('big-cart-banner', false, {
102
+ context: {
103
+ kind: 'user',
104
+ key: 'u_123',
105
+ cartValue: 120.5, // decimal MAJOR units of the project currency
106
+ viewedSkus: ['SKU-1', 'SKU-2'], // for "products viewed" matching on SKU
107
+ viewedCategories: ['trainers'], // for "products viewed" matching on category
108
+ purchaseHistory: {
109
+ // PurchaseHistoryAttribute — exported for your own declarations
110
+ hasPurchased: true,
111
+ lastPurchaseAt: 1765500000000, // epoch milliseconds (optional)
112
+ lifetimeSpend: 540.0, // decimal MAJOR units (optional)
113
+ orderCount: 7, // optional
114
+ },
115
+ },
116
+ })
117
+ ```
118
+
119
+ Server-side semantics to be aware of:
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) — 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
+
96
125
  ### `alias(previousContext, newContext)`
97
126
 
98
127
  Send an alias event linking two identities in the analytics pipeline. Typically called server-side when a user signs up or links an anonymous session to an account:
@@ -104,6 +133,10 @@ await server.alias(
104
133
  )
105
134
  ```
106
135
 
136
+ ### Sharing identity with the web snippet
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 — 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
+
107
140
  ---
108
141
 
109
142
  ## 5. Multi-context
package/dist/index.cjs CHANGED
@@ -40,6 +40,58 @@ module.exports = __toCommonJS(index_exports);
40
40
  var import_engine = require("@avsbhq/core/engine");
41
41
  var import_core2 = require("@avsbhq/core");
42
42
 
43
+ // src/datasets.ts
44
+ var DEFAULT_REQUEST_TIMEOUT = 1e4;
45
+ function miss(slug, version) {
46
+ return { found: false, meta: { slug, version } };
47
+ }
48
+ function createDatasetsClient(options) {
49
+ const { sdkKey, getDatafile, logger } = options;
50
+ const requestTimeout = options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
51
+ async function get(slug, key) {
52
+ if (!slug || !key) {
53
+ logger.warn("datasets.get called with an empty slug or key \u2014 returning not found");
54
+ return miss(slug, null);
55
+ }
56
+ const datafile = getDatafile();
57
+ if (!datafile) {
58
+ logger.debug(`SDK not ready, returning not found for dataset "${slug}"`);
59
+ return miss(slug, null);
60
+ }
61
+ const dataset = datafile.datasets?.find((d) => d.slug === slug);
62
+ if (!dataset) {
63
+ logger.debug(`Dataset "${slug}" not present in datafile \u2014 returning not found`);
64
+ return miss(slug, null);
65
+ }
66
+ if (!dataset.liveVersion) {
67
+ logger.debug(`Dataset "${slug}" has no live version \u2014 returning not found`);
68
+ return miss(slug, null);
69
+ }
70
+ const base = datafile.collectEndpoint.replace(/\/+$/, "");
71
+ const url = `${base}/v1/ds/${encodeURIComponent(sdkKey)}/${encodeURIComponent(slug)}/${dataset.liveVersion}/${encodeURIComponent(key)}`;
72
+ try {
73
+ const res = await fetch(url, { signal: AbortSignal.timeout(requestTimeout) });
74
+ if (!res.ok) {
75
+ logger.warn("Dataset lookup failed", { slug, key, status: res.status });
76
+ return miss(slug, dataset.liveVersion);
77
+ }
78
+ const body = await res.json();
79
+ if (!body || body.found !== true) {
80
+ return miss(slug, dataset.liveVersion);
81
+ }
82
+ return {
83
+ found: true,
84
+ value: body.value,
85
+ meta: { slug, version: dataset.liveVersion }
86
+ };
87
+ } catch (e) {
88
+ logger.warn("Dataset lookup error", { slug, key, error: String(e) });
89
+ return miss(slug, dataset.liveVersion);
90
+ }
91
+ }
92
+ return { get };
93
+ }
94
+
43
95
  // src/decisionLog.ts
44
96
  function createDecisionLog() {
45
97
  const entries = [];
@@ -415,6 +467,23 @@ var AvsbServer = class {
415
467
  }
416
468
  return this._utils;
417
469
  }
470
+ /**
471
+ * Server-side datasets read surface (e-commerce suite Phase 2).
472
+ * Constructed lazily on first access, mirroring the `utils` getter.
473
+ * `client.datasets.get(slug, key)` resolves the live version from the
474
+ * current datafile and never throws — see ./datasets.ts.
475
+ */
476
+ _datasets = null;
477
+ get datasets() {
478
+ if (!this._datasets) {
479
+ this._datasets = createDatasetsClient({
480
+ sdkKey: this.sdkKey,
481
+ getDatafile: () => this.datafile,
482
+ logger: this.logger
483
+ });
484
+ }
485
+ return this._datasets;
486
+ }
418
487
  constructor(options) {
419
488
  this.sdkKey = options.sdkKey;
420
489
  this.pollingInterval = Math.max(options.pollingInterval ?? 6e4, 5e3);
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { FlagDatafile, StickyBucketService, Logger, EvaluationSource, StickyAssignment, DecideOption, Flag, TrackPayload, DecisionLog, InitResult, EvalContext } from '@avsbhq/core';
2
- export { DecideOption, DecisionLog, DecisionLogEntry, EvalContext, EvaluationSource, ExposureEvent, Flag, FlagDatafile, FlagEvaluationDetails, InitResult, Logger, MetricEvent, MultiContext, RuleType, SingleContext, StickyAssignment, StickyBucketService, TrackPayload, consoleLogger, consoleTransport, createLogger, noopLogger } from '@avsbhq/core';
2
+ export { DatafileDataset, DatafileDatasetType, DecideOption, DecisionLog, DecisionLogEntry, EvalContext, EvaluationSource, ExposureEvent, Flag, FlagDatafile, FlagEvaluationDetails, InitResult, Logger, MetricEvent, MultiContext, 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
5
  export { StreamingClient, StreamingOptions, createStreamingClient } from '@avsbhq/utils/streaming';
@@ -89,6 +89,41 @@ interface PurchaseOrder {
89
89
  }>;
90
90
  }
91
91
 
92
+ /**
93
+ * Server-side datasets read client (e-commerce suite Phase 2).
94
+ *
95
+ * Resolves the live version of a dataset from the SDK's datafile
96
+ * (`datasets` field) and fetches a single key from the edge worker:
97
+ *
98
+ * GET {ingestionHost}/v1/ds/{sdkKey}/{slug}/{versionShortId}/{key}
99
+ *
100
+ * The ingestion host is derived from the datafile's `collectEndpoint`,
101
+ * exactly like the server tracker derives `/v1/collect/*`. The worker's
102
+ * key cache resolves SDK keys as well as snippet keys in the first path
103
+ * segment.
104
+ *
105
+ * Error posture matches flag evaluation: this client NEVER throws.
106
+ * Unknown slug, missing live version, SDK not ready, HTTP errors and
107
+ * network failures all degrade to `{ found: false }`.
108
+ */
109
+ interface DatasetGetResult {
110
+ found: boolean;
111
+ /** Parsed row for TABLE/FEED datasets; `true` for LIST membership hits. */
112
+ value?: unknown;
113
+ meta: {
114
+ slug: string;
115
+ /** Live version short id used for the lookup; null when unresolvable. */
116
+ version: number | null;
117
+ };
118
+ }
119
+ interface DatasetsClient {
120
+ /**
121
+ * Look up `key` in the live version of dataset `slug`.
122
+ * Resolves the version from the current datafile and never throws.
123
+ */
124
+ get(slug: string, key: string): Promise<DatasetGetResult>;
125
+ }
126
+
92
127
  /**
93
128
  * A per-request scoped client with a bound EvalContext.
94
129
  *
@@ -170,6 +205,14 @@ declare class AvsbServer {
170
205
  */
171
206
  private _utils;
172
207
  get utils(): AvsbUtils;
208
+ /**
209
+ * Server-side datasets read surface (e-commerce suite Phase 2).
210
+ * Constructed lazily on first access, mirroring the `utils` getter.
211
+ * `client.datasets.get(slug, key)` resolves the live version from the
212
+ * current datafile and never throws — see ./datasets.ts.
213
+ */
214
+ private _datasets;
215
+ get datasets(): DatasetsClient;
173
216
  constructor(options: AvsbServerOptions);
174
217
  onReady(): Promise<InitResult>;
175
218
  private initializeAndReady;
@@ -262,4 +305,4 @@ declare function createDecisionLog(): DecisionLog;
262
305
 
263
306
  declare const SDK_VERSION = "0.3.0";
264
307
 
265
- export { AvsbServer, type AvsbServerOptions, type EventOptions, type GetAllFlagsOptions, type GetFlagOptions, InMemoryStickyBucketService, type MiddlewareConfig, type PurchaseOrder, type RedisLike, type RedisStickyBucketOptions, RedisStickyBucketService, SDK_VERSION, type UserBoundClient, createDecisionLog, expressMiddleware, fastifyPlugin };
308
+ export { AvsbServer, type AvsbServerOptions, type DatasetGetResult, type DatasetsClient, type EventOptions, type GetAllFlagsOptions, type GetFlagOptions, InMemoryStickyBucketService, type MiddlewareConfig, type PurchaseOrder, type RedisLike, type RedisStickyBucketOptions, RedisStickyBucketService, SDK_VERSION, type UserBoundClient, createDecisionLog, expressMiddleware, fastifyPlugin };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { FlagDatafile, StickyBucketService, Logger, EvaluationSource, StickyAssignment, DecideOption, Flag, TrackPayload, DecisionLog, InitResult, EvalContext } from '@avsbhq/core';
2
- export { DecideOption, DecisionLog, DecisionLogEntry, EvalContext, EvaluationSource, ExposureEvent, Flag, FlagDatafile, FlagEvaluationDetails, InitResult, Logger, MetricEvent, MultiContext, RuleType, SingleContext, StickyAssignment, StickyBucketService, TrackPayload, consoleLogger, consoleTransport, createLogger, noopLogger } from '@avsbhq/core';
2
+ export { DatafileDataset, DatafileDatasetType, DecideOption, DecisionLog, DecisionLogEntry, EvalContext, EvaluationSource, ExposureEvent, Flag, FlagDatafile, FlagEvaluationDetails, InitResult, Logger, MetricEvent, MultiContext, 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
5
  export { StreamingClient, StreamingOptions, createStreamingClient } from '@avsbhq/utils/streaming';
@@ -89,6 +89,41 @@ interface PurchaseOrder {
89
89
  }>;
90
90
  }
91
91
 
92
+ /**
93
+ * Server-side datasets read client (e-commerce suite Phase 2).
94
+ *
95
+ * Resolves the live version of a dataset from the SDK's datafile
96
+ * (`datasets` field) and fetches a single key from the edge worker:
97
+ *
98
+ * GET {ingestionHost}/v1/ds/{sdkKey}/{slug}/{versionShortId}/{key}
99
+ *
100
+ * The ingestion host is derived from the datafile's `collectEndpoint`,
101
+ * exactly like the server tracker derives `/v1/collect/*`. The worker's
102
+ * key cache resolves SDK keys as well as snippet keys in the first path
103
+ * segment.
104
+ *
105
+ * Error posture matches flag evaluation: this client NEVER throws.
106
+ * Unknown slug, missing live version, SDK not ready, HTTP errors and
107
+ * network failures all degrade to `{ found: false }`.
108
+ */
109
+ interface DatasetGetResult {
110
+ found: boolean;
111
+ /** Parsed row for TABLE/FEED datasets; `true` for LIST membership hits. */
112
+ value?: unknown;
113
+ meta: {
114
+ slug: string;
115
+ /** Live version short id used for the lookup; null when unresolvable. */
116
+ version: number | null;
117
+ };
118
+ }
119
+ interface DatasetsClient {
120
+ /**
121
+ * Look up `key` in the live version of dataset `slug`.
122
+ * Resolves the version from the current datafile and never throws.
123
+ */
124
+ get(slug: string, key: string): Promise<DatasetGetResult>;
125
+ }
126
+
92
127
  /**
93
128
  * A per-request scoped client with a bound EvalContext.
94
129
  *
@@ -170,6 +205,14 @@ declare class AvsbServer {
170
205
  */
171
206
  private _utils;
172
207
  get utils(): AvsbUtils;
208
+ /**
209
+ * Server-side datasets read surface (e-commerce suite Phase 2).
210
+ * Constructed lazily on first access, mirroring the `utils` getter.
211
+ * `client.datasets.get(slug, key)` resolves the live version from the
212
+ * current datafile and never throws — see ./datasets.ts.
213
+ */
214
+ private _datasets;
215
+ get datasets(): DatasetsClient;
173
216
  constructor(options: AvsbServerOptions);
174
217
  onReady(): Promise<InitResult>;
175
218
  private initializeAndReady;
@@ -262,4 +305,4 @@ declare function createDecisionLog(): DecisionLog;
262
305
 
263
306
  declare const SDK_VERSION = "0.3.0";
264
307
 
265
- export { AvsbServer, type AvsbServerOptions, type EventOptions, type GetAllFlagsOptions, type GetFlagOptions, InMemoryStickyBucketService, type MiddlewareConfig, type PurchaseOrder, type RedisLike, type RedisStickyBucketOptions, RedisStickyBucketService, SDK_VERSION, type UserBoundClient, createDecisionLog, expressMiddleware, fastifyPlugin };
308
+ export { AvsbServer, type AvsbServerOptions, type DatasetGetResult, type DatasetsClient, type EventOptions, type GetAllFlagsOptions, type GetFlagOptions, InMemoryStickyBucketService, type MiddlewareConfig, type PurchaseOrder, type RedisLike, type RedisStickyBucketOptions, RedisStickyBucketService, SDK_VERSION, type UserBoundClient, createDecisionLog, expressMiddleware, fastifyPlugin };
package/dist/index.js CHANGED
@@ -13,6 +13,58 @@ import {
13
13
  noopLogger
14
14
  } from "@avsbhq/core";
15
15
 
16
+ // src/datasets.ts
17
+ var DEFAULT_REQUEST_TIMEOUT = 1e4;
18
+ function miss(slug, version) {
19
+ return { found: false, meta: { slug, version } };
20
+ }
21
+ function createDatasetsClient(options) {
22
+ const { sdkKey, getDatafile, logger } = options;
23
+ const requestTimeout = options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
24
+ async function get(slug, key) {
25
+ if (!slug || !key) {
26
+ logger.warn("datasets.get called with an empty slug or key \u2014 returning not found");
27
+ return miss(slug, null);
28
+ }
29
+ const datafile = getDatafile();
30
+ if (!datafile) {
31
+ logger.debug(`SDK not ready, returning not found for dataset "${slug}"`);
32
+ return miss(slug, null);
33
+ }
34
+ const dataset = datafile.datasets?.find((d) => d.slug === slug);
35
+ if (!dataset) {
36
+ logger.debug(`Dataset "${slug}" not present in datafile \u2014 returning not found`);
37
+ return miss(slug, null);
38
+ }
39
+ if (!dataset.liveVersion) {
40
+ logger.debug(`Dataset "${slug}" has no live version \u2014 returning not found`);
41
+ return miss(slug, null);
42
+ }
43
+ const base = datafile.collectEndpoint.replace(/\/+$/, "");
44
+ const url = `${base}/v1/ds/${encodeURIComponent(sdkKey)}/${encodeURIComponent(slug)}/${dataset.liveVersion}/${encodeURIComponent(key)}`;
45
+ try {
46
+ const res = await fetch(url, { signal: AbortSignal.timeout(requestTimeout) });
47
+ if (!res.ok) {
48
+ logger.warn("Dataset lookup failed", { slug, key, status: res.status });
49
+ return miss(slug, dataset.liveVersion);
50
+ }
51
+ const body = await res.json();
52
+ if (!body || body.found !== true) {
53
+ return miss(slug, dataset.liveVersion);
54
+ }
55
+ return {
56
+ found: true,
57
+ value: body.value,
58
+ meta: { slug, version: dataset.liveVersion }
59
+ };
60
+ } catch (e) {
61
+ logger.warn("Dataset lookup error", { slug, key, error: String(e) });
62
+ return miss(slug, dataset.liveVersion);
63
+ }
64
+ }
65
+ return { get };
66
+ }
67
+
16
68
  // src/decisionLog.ts
17
69
  function createDecisionLog() {
18
70
  const entries = [];
@@ -391,6 +443,23 @@ var AvsbServer = class {
391
443
  }
392
444
  return this._utils;
393
445
  }
446
+ /**
447
+ * Server-side datasets read surface (e-commerce suite Phase 2).
448
+ * Constructed lazily on first access, mirroring the `utils` getter.
449
+ * `client.datasets.get(slug, key)` resolves the live version from the
450
+ * current datafile and never throws — see ./datasets.ts.
451
+ */
452
+ _datasets = null;
453
+ get datasets() {
454
+ if (!this._datasets) {
455
+ this._datasets = createDatasetsClient({
456
+ sdkKey: this.sdkKey,
457
+ getDatafile: () => this.datafile,
458
+ logger: this.logger
459
+ });
460
+ }
461
+ return this._datasets;
462
+ }
394
463
  constructor(options) {
395
464
  this.sdkKey = options.sdkKey;
396
465
  this.pollingInterval = Math.max(options.pollingInterval ?? 6e4, 5e3);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avsbhq/node",
3
- "version": "1.2.0",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
@@ -33,7 +33,7 @@
33
33
  "test:watch": "vitest"
34
34
  },
35
35
  "dependencies": {
36
- "@avsbhq/core": "1.1.0",
36
+ "@avsbhq/core": "1.3.0",
37
37
  "@avsbhq/utils": "1.0.1"
38
38
  },
39
39
  "devDependencies": {