@avsbhq/node 1.7.2 → 1.8.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
@@ -35,8 +35,8 @@ if (!result.success) {
35
35
  // Bind the context once per request.
36
36
  const user = server.forUser({ kind: 'user', key: 'u_123', plan: 'pro' });
37
37
 
38
- const flag = user.getBoolFlag('new-checkout-flow', false);
39
- const checkoutTemplate = flag.isEnabled() ? 'checkout-v2' : 'checkout-v1';
38
+ const flag = user.getBoolFlag('new_checkout_flow', false);
39
+ const checkoutTemplate = flag.isEnabled() ? 'checkout_v2' : 'checkout_v1';
40
40
 
41
41
  user.track('checkout_started', { revenue: 99.0 });
42
42
  ```
@@ -139,7 +139,7 @@ const user = server.forUser({
139
139
  country: req.user.country,
140
140
  });
141
141
 
142
- const flag = user.getBoolFlag('checkout-v2', false);
142
+ const flag = user.getBoolFlag('checkout_v2', false);
143
143
  user.track('page_viewed');
144
144
  ```
145
145
 
@@ -150,7 +150,7 @@ user.track('page_viewed');
150
150
  Context is the THIRD POSITIONAL argument on every evaluation call, matching `@avsbhq/edge`:
151
151
 
152
152
  ```ts
153
- const flag = server.getBoolFlag('checkout-v2', false, {
153
+ const flag = server.getBoolFlag('checkout_v2', false, {
154
154
  kind: 'user',
155
155
  key: 'u_123',
156
156
  plan: 'pro',
@@ -175,7 +175,7 @@ const user = server.forUser({
175
175
  Audiences built with the commerce condition types (cart value, products viewed, purchase history) read fixed attribute names off the context:
176
176
 
177
177
  ```ts
178
- const flag = server.getBoolFlag('big-cart-banner', false, {
178
+ const flag = server.getBoolFlag('big_cart_banner', false, {
179
179
  kind: 'user',
180
180
  key: 'u_123',
181
181
  cartValue: 120.5, // decimal MAJOR units of the project currency
@@ -290,7 +290,7 @@ interface Flag<T = unknown> {
290
290
  ```ts
291
291
  import { DecideOption } from '@avsbhq/node';
292
292
 
293
- user.getBoolFlag('checkout-v2', false, {
293
+ user.getBoolFlag('checkout_v2', false, {
294
294
  decideOptions: [DecideOption.DISABLE_EXPOSURE],
295
295
  });
296
296
  ```
@@ -314,12 +314,12 @@ Called without a context, or before the datafile loads, it returns `{}` AND warn
314
314
  ### Overrides, for QA and testing
315
315
 
316
316
  ```ts
317
- server.setOverrideForUser('checkout-v2', 'u_qa', 'on');
318
- server.setOverrideForUser('checkout-v2', 'acme', 'on', 'organization'); // explicit kind
319
- server.setGlobalOverride('checkout-v2', 'on');
317
+ server.setOverrideForUser('checkout_v2', 'u_qa', 'on');
318
+ server.setOverrideForUser('checkout_v2', 'acme', 'on', 'organization'); // explicit kind
319
+ server.setGlobalOverride('checkout_v2', 'on');
320
320
 
321
- server.clearOverrideForUser('checkout-v2', 'u_qa');
322
- server.clearGlobalOverride('checkout-v2');
321
+ server.clearOverrideForUser('checkout_v2', 'u_qa');
322
+ server.clearGlobalOverride('checkout_v2');
323
323
  server.clearAllOverrides();
324
324
  ```
325
325
 
@@ -327,8 +327,8 @@ Per-request forced decisions, which never touch the shared state above:
327
327
 
328
328
  ```ts
329
329
  const user = server.forUser({ kind: 'user', key: 'u_qa' });
330
- user.setForcedDecision('checkout-v2', { variationKey: 'on' });
331
- user.clearForcedDecision('checkout-v2');
330
+ user.setForcedDecision('checkout_v2', { variationKey: 'on' });
331
+ user.clearForcedDecision('checkout_v2');
332
332
  ```
333
333
 
334
334
  ---
@@ -380,12 +380,12 @@ Purchases are queued before ready exactly like `track`. They used to be dropped
380
380
  ### Manual exposure
381
381
 
382
382
  ```ts
383
- const flag = user.getBoolFlag('checkout-v2', false, {
383
+ const flag = user.getBoolFlag('checkout_v2', false, {
384
384
  decideOptions: [DecideOption.DISABLE_EXPOSURE],
385
385
  });
386
386
 
387
387
  // Later, at the point the visitor actually sees the variation:
388
- user.manualExposure('checkout-v2');
388
+ user.manualExposure('checkout_v2');
389
389
  ```
390
390
 
391
391
  ---
@@ -474,7 +474,7 @@ actually being served.
474
474
  - **Where it shows up**: Environments in the project sidebar, in the dashboard. Each
475
475
  environment shows Connected, Stale, or Not connected, plus the `sdkType` and
476
476
  `sdkVersion` that last reported, which is how you see "production is running
477
- @avsbhq/node 1.6.2". An environment goes Stale after five minutes without a
477
+ @avsbhq/node 1.8.0". An environment goes Stale after five minutes without a
478
478
  heartbeat.
479
479
  - **Turning it off**: publish a datafile with no `heartbeatEndpoint`. The SDK
480
480
  never guesses a host, so an absent field simply means no heartbeats.
@@ -587,7 +587,7 @@ import { createMockServer, flagsFromTestData, TestData } from '@avsbhq/test';
587
587
 
588
588
  const server = createMockServer(
589
589
  flagsFromTestData([
590
- TestData.flag('checkout-v2')
590
+ TestData.flag('checkout_v2')
591
591
  .booleanFlag()
592
592
  .variationForUser('u_paying', true)
593
593
  .fallthroughVariation(false)
@@ -596,7 +596,7 @@ const server = createMockServer(
596
596
  );
597
597
 
598
598
  const user = server.forUser({ kind: 'user', key: 'u_paying' });
599
- expect(user.getBoolFlag('checkout-v2', false).value).toBe(true);
599
+ expect(user.getBoolFlag('checkout_v2', false).value).toBe(true);
600
600
  expect(server._evaluations()).toHaveLength(1);
601
601
  ```
602
602
 
@@ -630,7 +630,7 @@ app.use(
630
630
 
631
631
  // Deep inside a service, with no `req` in scope:
632
632
  function pricingService() {
633
- return getRequestClient()?.getBoolFlag('new-pricing', false).value ?? false;
633
+ return getRequestClient()?.getBoolFlag('new_pricing', false).value ?? false;
634
634
  }
635
635
  ```
636
636
 
@@ -640,8 +640,8 @@ Every route then reads flags off `req.avsb`:
640
640
  // docs-example: not typechecked here, because express is not a dependency of the A vs B repository, so a route handler's Request and Response types cannot be resolved
641
641
 
642
642
  app.get('/checkout', (req, res) => {
643
- const flag = req.avsb.getBoolFlag('new-checkout-flow', false);
644
- res.render(flag.isEnabled() ? 'checkout-v2' : 'checkout-v1');
643
+ const flag = req.avsb.getBoolFlag('new_checkout_flow', false);
644
+ res.render(flag.isEnabled() ? 'checkout_v2' : 'checkout_v1');
645
645
  });
646
646
  ```
647
647
 
@@ -682,7 +682,7 @@ await app.register(fastifyPlugin, {
682
682
  });
683
683
 
684
684
  app.get('/features', (req, reply) => {
685
- reply.send({ enabled: req.avsb.getBoolFlag('new-feature', false).isEnabled() });
685
+ reply.send({ enabled: req.avsb.getBoolFlag('new_feature', false).isEnabled() });
686
686
  });
687
687
  ```
688
688
 
@@ -716,7 +716,7 @@ The Redis service keeps evaluation synchronous: a lookup is a cache hit or null,
716
716
 
717
717
  ```ts
718
718
  const user = server.forUser({ kind: 'user', key: 'u_123' });
719
- user.getBoolFlag('checkout-v2', false);
719
+ user.getBoolFlag('checkout_v2', false);
720
720
 
721
721
  const entries = user.getDecisionLog().entries;
722
722
  // [{ flagKey, value, variationKey, source, ruleId, ruleType, reasons,
package/dist/index.cjs CHANGED
@@ -482,8 +482,15 @@ async function fetchDatafile(input) {
482
482
  );
483
483
  return { status: "failed" };
484
484
  }
485
- const datafile = await response.json();
486
- return { status: "ok", datafile, etag: response.headers.get("etag") };
485
+ const body = await response.json();
486
+ const problem = (0, import_core3.datafileShapeProblem)(body);
487
+ if (problem) {
488
+ input.logger.error(
489
+ `AvsB datafile from ${url} is not a datafile the SDK can read: ${problem}. The SDK keeps serving whatever datafile it already has and retries on the polling interval.`
490
+ );
491
+ return { status: "failed" };
492
+ }
493
+ return { status: "ok", datafile: body, etag: response.headers.get("etag") };
487
494
  } catch (error) {
488
495
  if (input.signal.aborted) return { status: "aborted" };
489
496
  input.logger.error(
@@ -492,6 +499,14 @@ async function fetchDatafile(input) {
492
499
  return { status: "failed" };
493
500
  }
494
501
  }
502
+ function acceptDatafile(candidate, origin, logger) {
503
+ const problem = (0, import_core3.datafileShapeProblem)(candidate);
504
+ if (problem === null) return candidate;
505
+ logger.error(
506
+ origin === "bootstrap" ? `AvsB ignored the bootstrap datafile passed to new AvsbServer(): ${problem}. Flags return the default you passed, with source "not_ready", until a datafile loads from the CDN. Pass the datafile JSON exactly as the CDN serves it for this SDK key.` : `AvsB ignored the datafile passed to applyDatafileBootstrap(): ${problem}. The SDK keeps serving the datafile it already has, if any. Pass the datafile JSON exactly as the CDN serves it for this SDK key.`
507
+ );
508
+ return null;
509
+ }
495
510
  function sendHeartbeat(input) {
496
511
  const endpoint = (0, import_core3.resolveHeartbeatEndpoint)(input.datafileHeartbeatEndpoint);
497
512
  if (!endpoint) return null;
@@ -701,7 +716,8 @@ function createServerTracker(options) {
701
716
  var import_streaming = require("@avsbhq/utils/streaming");
702
717
 
703
718
  // src/version.ts
704
- var SDK_VERSION = "1.7.1";
719
+ var SDK_VERSION = "1.8.0";
720
+ var SDK_TYPE = "@avsbhq/node";
705
721
 
706
722
  // src/UserBoundClient.ts
707
723
  var import_core6 = require("@avsbhq/core");
@@ -811,6 +827,23 @@ function buildServerUtils(server) {
811
827
  return (0, import_utils.createUtils)(adapter);
812
828
  }
813
829
 
830
+ // src/initTimeout.ts
831
+ async function raceInitTimeout(work, timeoutMs) {
832
+ let timer;
833
+ const timeout = new Promise((resolve) => {
834
+ timer = setTimeout(() => resolve({ timedOut: true }), timeoutMs);
835
+ if (typeof timer === "object" && timer !== null && "unref" in timer) timer.unref();
836
+ });
837
+ try {
838
+ return await Promise.race([
839
+ work.then((value) => ({ timedOut: false, value })),
840
+ timeout
841
+ ]);
842
+ } finally {
843
+ clearTimeout(timer);
844
+ }
845
+ }
846
+
814
847
  // src/AvsbServer.ts
815
848
  var DEFAULT_EVENTS = {
816
849
  flushInterval: 2e3,
@@ -818,7 +851,6 @@ var DEFAULT_EVENTS = {
818
851
  maxQueueSize: 1e4,
819
852
  retryAttempts: 3
820
853
  };
821
- var SDK_TYPE = "@avsbhq/node";
822
854
  var HEARTBEAT_MIN_INTERVAL_MS = 12e4;
823
855
  var AvsbServer = class {
824
856
  sdkKey;
@@ -915,8 +947,8 @@ var AvsbServer = class {
915
947
  this.eventOptions = { ...DEFAULT_EVENTS, ...options.events };
916
948
  this.streamingEnabled = options.streaming !== false;
917
949
  this.streamingEndpointOption = options.streamingEndpoint ?? null;
918
- this.datafile = options.bootstrap ?? null;
919
- this.ready = !!options.bootstrap;
950
+ this.datafile = options.bootstrap ? acceptDatafile(options.bootstrap, "bootstrap", this.logger) : null;
951
+ this.ready = this.datafile !== null;
920
952
  this.destroyed = false;
921
953
  this.tracker = null;
922
954
  this.streamingClient = null;
@@ -950,21 +982,13 @@ var AvsbServer = class {
950
982
  return { success: true, source: "bootstrap" };
951
983
  }
952
984
  try {
953
- const outcome = await Promise.race([
985
+ const outcome = await raceInitTimeout(
954
986
  // At init there is no stored etag, so 'unchanged' cannot happen here;
955
987
  // it is folded in anyway so a future caching layer cannot make init
956
988
  // silently report failure.
957
- this.loadDatafile().then((result) => ({
958
- applied: result !== "failed",
959
- timedOut: false
960
- })),
961
- new Promise(
962
- (resolve) => setTimeout(
963
- () => resolve({ applied: false, timedOut: true }),
964
- this.initTimeout
965
- )
966
- )
967
- ]);
989
+ this.loadDatafile().then((result) => result !== "failed"),
990
+ this.initTimeout
991
+ );
968
992
  if (outcome.timedOut) {
969
993
  this.startPolling();
970
994
  const error2 = new Error(
@@ -973,7 +997,7 @@ var AvsbServer = class {
973
997
  this.emit("error", error2);
974
998
  return { success: false, source: "timeout", error: error2 };
975
999
  }
976
- if (outcome.applied) {
1000
+ if (outcome.value) {
977
1001
  this.onDatafileReadable();
978
1002
  return { success: true, source: "network" };
979
1003
  }
@@ -1051,6 +1075,7 @@ var AvsbServer = class {
1051
1075
  */
1052
1076
  applyDatafileBootstrap(datafile) {
1053
1077
  if (this.destroyed) return;
1078
+ if (!acceptDatafile(datafile, "applyDatafileBootstrap", this.logger)) return;
1054
1079
  const changed = this.datafile?.publishedAt !== datafile.publishedAt;
1055
1080
  this.datafile = datafile;
1056
1081
  this.datafileEtag = null;
@@ -1146,6 +1171,9 @@ var AvsbServer = class {
1146
1171
  evaluateAndBuild(flagKey, defaultValue, context, decide, options) {
1147
1172
  const legacyCtx = this.buildLegacyContext(context, decide.suppressExposure);
1148
1173
  const result = this.evaluate(flagKey, legacyCtx, defaultValue);
1174
+ if (result.source === "not_found") {
1175
+ this.warnOnce(`missing:${flagKey}`, (0, import_core8.flagNotFoundMessage)(flagKey));
1176
+ }
1149
1177
  const typeMismatch = this.checkType(flagKey, options?.expectedType, result);
1150
1178
  if (typeMismatch) {
1151
1179
  this.logger.warn(
package/dist/index.d.cts CHANGED
@@ -676,6 +676,7 @@ type WebhookVerifyResult = {
676
676
  */
677
677
  declare function verifyWebhookSignature(payload: string | Buffer, headers: WebhookHeaders, secret: string, options?: VerifyWebhookOptions): WebhookVerifyResult;
678
678
 
679
- declare const SDK_VERSION = "1.7.1";
679
+ /** This package's version, reported on every liveness heartbeat as `sdkVersion`. */
680
+ declare const SDK_VERSION = "1.8.0";
680
681
 
681
682
  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 };
package/dist/index.d.ts CHANGED
@@ -676,6 +676,7 @@ type WebhookVerifyResult = {
676
676
  */
677
677
  declare function verifyWebhookSignature(payload: string | Buffer, headers: WebhookHeaders, secret: string, options?: VerifyWebhookOptions): WebhookVerifyResult;
678
678
 
679
- declare const SDK_VERSION = "1.7.1";
679
+ /** This package's version, reported on every liveness heartbeat as `sdkVersion`. */
680
+ declare const SDK_VERSION = "1.8.0";
680
681
 
681
682
  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 };
package/dist/index.js CHANGED
@@ -12,7 +12,8 @@ import {
12
12
  contextSummary,
13
13
  privateAttributesFrom,
14
14
  bucketingKey as bucketingKey2,
15
- resolveStreamEndpoint
15
+ resolveStreamEndpoint,
16
+ flagNotFoundMessage
16
17
  } from "@avsbhq/core";
17
18
  import { validateSdkKeyShape } from "@avsbhq/utils/sdkKey";
18
19
  import { jitteredInterval } from "@avsbhq/utils/backoff";
@@ -427,7 +428,7 @@ function createOverrideStore() {
427
428
  }
428
429
 
429
430
  // src/datafileSource.ts
430
- import { datafileUrl, resolveHeartbeatEndpoint } from "@avsbhq/core";
431
+ import { datafileShapeProblem, datafileUrl, resolveHeartbeatEndpoint } from "@avsbhq/core";
431
432
  async function fetchDatafile(input) {
432
433
  const url = datafileUrl(input.sdkKey, input.cdnHost);
433
434
  const headers = {};
@@ -441,8 +442,15 @@ async function fetchDatafile(input) {
441
442
  );
442
443
  return { status: "failed" };
443
444
  }
444
- const datafile = await response.json();
445
- return { status: "ok", datafile, etag: response.headers.get("etag") };
445
+ const body = await response.json();
446
+ const problem = datafileShapeProblem(body);
447
+ if (problem) {
448
+ input.logger.error(
449
+ `AvsB datafile from ${url} is not a datafile the SDK can read: ${problem}. The SDK keeps serving whatever datafile it already has and retries on the polling interval.`
450
+ );
451
+ return { status: "failed" };
452
+ }
453
+ return { status: "ok", datafile: body, etag: response.headers.get("etag") };
446
454
  } catch (error) {
447
455
  if (input.signal.aborted) return { status: "aborted" };
448
456
  input.logger.error(
@@ -451,6 +459,14 @@ async function fetchDatafile(input) {
451
459
  return { status: "failed" };
452
460
  }
453
461
  }
462
+ function acceptDatafile(candidate, origin, logger) {
463
+ const problem = datafileShapeProblem(candidate);
464
+ if (problem === null) return candidate;
465
+ logger.error(
466
+ origin === "bootstrap" ? `AvsB ignored the bootstrap datafile passed to new AvsbServer(): ${problem}. Flags return the default you passed, with source "not_ready", until a datafile loads from the CDN. Pass the datafile JSON exactly as the CDN serves it for this SDK key.` : `AvsB ignored the datafile passed to applyDatafileBootstrap(): ${problem}. The SDK keeps serving the datafile it already has, if any. Pass the datafile JSON exactly as the CDN serves it for this SDK key.`
467
+ );
468
+ return null;
469
+ }
454
470
  function sendHeartbeat(input) {
455
471
  const endpoint = resolveHeartbeatEndpoint(input.datafileHeartbeatEndpoint);
456
472
  if (!endpoint) return null;
@@ -667,7 +683,8 @@ import {
667
683
  } from "@avsbhq/utils/streaming";
668
684
 
669
685
  // src/version.ts
670
- var SDK_VERSION = "1.7.1";
686
+ var SDK_VERSION = "1.8.0";
687
+ var SDK_TYPE = "@avsbhq/node";
671
688
 
672
689
  // src/UserBoundClient.ts
673
690
  import { bucketingKey } from "@avsbhq/core";
@@ -777,6 +794,23 @@ function buildServerUtils(server) {
777
794
  return createUtils(adapter);
778
795
  }
779
796
 
797
+ // src/initTimeout.ts
798
+ async function raceInitTimeout(work, timeoutMs) {
799
+ let timer;
800
+ const timeout = new Promise((resolve) => {
801
+ timer = setTimeout(() => resolve({ timedOut: true }), timeoutMs);
802
+ if (typeof timer === "object" && timer !== null && "unref" in timer) timer.unref();
803
+ });
804
+ try {
805
+ return await Promise.race([
806
+ work.then((value) => ({ timedOut: false, value })),
807
+ timeout
808
+ ]);
809
+ } finally {
810
+ clearTimeout(timer);
811
+ }
812
+ }
813
+
780
814
  // src/AvsbServer.ts
781
815
  var DEFAULT_EVENTS = {
782
816
  flushInterval: 2e3,
@@ -784,7 +818,6 @@ var DEFAULT_EVENTS = {
784
818
  maxQueueSize: 1e4,
785
819
  retryAttempts: 3
786
820
  };
787
- var SDK_TYPE = "@avsbhq/node";
788
821
  var HEARTBEAT_MIN_INTERVAL_MS = 12e4;
789
822
  var AvsbServer = class {
790
823
  sdkKey;
@@ -881,8 +914,8 @@ var AvsbServer = class {
881
914
  this.eventOptions = { ...DEFAULT_EVENTS, ...options.events };
882
915
  this.streamingEnabled = options.streaming !== false;
883
916
  this.streamingEndpointOption = options.streamingEndpoint ?? null;
884
- this.datafile = options.bootstrap ?? null;
885
- this.ready = !!options.bootstrap;
917
+ this.datafile = options.bootstrap ? acceptDatafile(options.bootstrap, "bootstrap", this.logger) : null;
918
+ this.ready = this.datafile !== null;
886
919
  this.destroyed = false;
887
920
  this.tracker = null;
888
921
  this.streamingClient = null;
@@ -916,21 +949,13 @@ var AvsbServer = class {
916
949
  return { success: true, source: "bootstrap" };
917
950
  }
918
951
  try {
919
- const outcome = await Promise.race([
952
+ const outcome = await raceInitTimeout(
920
953
  // At init there is no stored etag, so 'unchanged' cannot happen here;
921
954
  // it is folded in anyway so a future caching layer cannot make init
922
955
  // silently report failure.
923
- this.loadDatafile().then((result) => ({
924
- applied: result !== "failed",
925
- timedOut: false
926
- })),
927
- new Promise(
928
- (resolve) => setTimeout(
929
- () => resolve({ applied: false, timedOut: true }),
930
- this.initTimeout
931
- )
932
- )
933
- ]);
956
+ this.loadDatafile().then((result) => result !== "failed"),
957
+ this.initTimeout
958
+ );
934
959
  if (outcome.timedOut) {
935
960
  this.startPolling();
936
961
  const error2 = new Error(
@@ -939,7 +964,7 @@ var AvsbServer = class {
939
964
  this.emit("error", error2);
940
965
  return { success: false, source: "timeout", error: error2 };
941
966
  }
942
- if (outcome.applied) {
967
+ if (outcome.value) {
943
968
  this.onDatafileReadable();
944
969
  return { success: true, source: "network" };
945
970
  }
@@ -1017,6 +1042,7 @@ var AvsbServer = class {
1017
1042
  */
1018
1043
  applyDatafileBootstrap(datafile) {
1019
1044
  if (this.destroyed) return;
1045
+ if (!acceptDatafile(datafile, "applyDatafileBootstrap", this.logger)) return;
1020
1046
  const changed = this.datafile?.publishedAt !== datafile.publishedAt;
1021
1047
  this.datafile = datafile;
1022
1048
  this.datafileEtag = null;
@@ -1112,6 +1138,9 @@ var AvsbServer = class {
1112
1138
  evaluateAndBuild(flagKey, defaultValue, context, decide, options) {
1113
1139
  const legacyCtx = this.buildLegacyContext(context, decide.suppressExposure);
1114
1140
  const result = this.evaluate(flagKey, legacyCtx, defaultValue);
1141
+ if (result.source === "not_found") {
1142
+ this.warnOnce(`missing:${flagKey}`, flagNotFoundMessage(flagKey));
1143
+ }
1115
1144
  const typeMismatch = this.checkType(flagKey, options?.expectedType, result);
1116
1145
  if (typeMismatch) {
1117
1146
  this.logger.warn(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avsbhq/node",
3
- "version": "1.7.2",
3
+ "version": "1.8.0",
4
4
  "description": "Node.js server SDK for A vs B feature flags and experiments: per-request contexts, sticky bucketing, decision logs, and batched event delivery.",
5
5
  "keywords": [
6
6
  "avsb",
@@ -49,14 +49,15 @@
49
49
  "url": "https://github.com/avsbhq/a-vs-b/issues"
50
50
  },
51
51
  "scripts": {
52
+ "prebuild": "node ../avsb-core/scripts/writeSdkVersion.mjs",
52
53
  "build": "tsup",
53
54
  "dev": "tsup --watch",
54
55
  "test": "vitest run",
55
56
  "test:watch": "vitest"
56
57
  },
57
58
  "dependencies": {
58
- "@avsbhq/core": "^1.4.0",
59
- "@avsbhq/utils": "^1.0.2"
59
+ "@avsbhq/core": "^1.6.0",
60
+ "@avsbhq/utils": "^1.2.1"
60
61
  },
61
62
  "devDependencies": {
62
63
  "@types/node": "^20.0.0",