@vercel/flags-core 1.5.0 → 1.6.0-7d29945-20260616152249

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.
@@ -50,6 +50,22 @@ var MAX_REGEX_INPUT_LENGTH = 1e4;
50
50
  var UINT32_MAX = 4294967295;
51
51
  var SCALED_WEIGHTS = /* @__PURE__ */ Symbol("@vercel/flags-core:scaledWeights");
52
52
  var COMPILED_REGEX = /* @__PURE__ */ Symbol("@vercel/flags-core:compiledRegex");
53
+ var EVALUATION_VARIANT_INDEX = /* @__PURE__ */ Symbol(
54
+ "@vercel/flags-core:evaluationVariantIndex"
55
+ );
56
+ function withVariantIndex(value, outcomeType, variantIndex) {
57
+ return Object.defineProperty(
58
+ {
59
+ value,
60
+ outcomeType
61
+ },
62
+ EVALUATION_VARIANT_INDEX,
63
+ { value: variantIndex }
64
+ );
65
+ }
66
+ function getEvaluationVariantIndex(result) {
67
+ return result[EVALUATION_VARIANT_INDEX];
68
+ }
53
69
  function getScaledWeights(outcome) {
54
70
  const cached = outcome[SCALED_WEIGHTS];
55
71
  if (cached) return cached;
@@ -313,10 +329,11 @@ function getVariant(variants, index) {
313
329
  }
314
330
  function handleOutcome(params, outcome) {
315
331
  if (typeof outcome === "number") {
316
- return {
317
- value: getVariant(params.definition.variants, outcome),
318
- outcomeType: "value" /* VALUE */
319
- };
332
+ return withVariantIndex(
333
+ getVariant(params.definition.variants, outcome),
334
+ "value" /* VALUE */,
335
+ outcome
336
+ );
320
337
  }
321
338
  switch (outcome.type) {
322
339
  case "split": {
@@ -326,15 +343,21 @@ function handleOutcome(params, outcome) {
326
343
  outcome.defaultVariant
327
344
  );
328
345
  if (typeof lhs !== "string") {
329
- return { value: defaultOutcome, outcomeType: "split" /* SPLIT */ };
346
+ return withVariantIndex(
347
+ defaultOutcome,
348
+ "split" /* SPLIT */,
349
+ outcome.defaultVariant
350
+ );
330
351
  }
331
352
  const value = hashInput(lhs, params.definition.seed);
332
353
  const scaledWeights = getScaledWeights(outcome);
333
354
  const variantIndex = findWeightedIndex(scaledWeights, value, UINT32_MAX);
334
- return {
335
- value: variantIndex === -1 ? defaultOutcome : getVariant(params.definition.variants, variantIndex),
336
- outcomeType: "split" /* SPLIT */
337
- };
355
+ const selectedVariantIndex = variantIndex === -1 ? outcome.defaultVariant : variantIndex;
356
+ return withVariantIndex(
357
+ selectedVariantIndex === outcome.defaultVariant ? defaultOutcome : getVariant(params.definition.variants, selectedVariantIndex),
358
+ "split" /* SPLIT */,
359
+ selectedVariantIndex
360
+ );
338
361
  }
339
362
  case "rollout": {
340
363
  const lhs = access(outcome.base, params);
@@ -343,18 +366,20 @@ function handleOutcome(params, outcome) {
343
366
  outcome.defaultVariant
344
367
  );
345
368
  if (typeof lhs !== "string") {
346
- return { value: defaultOutcome, outcomeType: "rollout" /* ROLLOUT */ };
369
+ return withVariantIndex(
370
+ defaultOutcome,
371
+ "rollout" /* ROLLOUT */,
372
+ outcome.defaultVariant
373
+ );
347
374
  }
348
375
  const now = Date.now();
349
376
  const elapsed = now - outcome.startTimestamp;
350
377
  if (elapsed < 0 || outcome.slots.length === 0) {
351
- return {
352
- value: getVariant(
353
- params.definition.variants,
354
- outcome.rollFromVariant
355
- ),
356
- outcomeType: "rollout" /* ROLLOUT */
357
- };
378
+ return withVariantIndex(
379
+ getVariant(params.definition.variants, outcome.rollFromVariant),
380
+ "rollout" /* ROLLOUT */,
381
+ outcome.rollFromVariant
382
+ );
358
383
  }
359
384
  let cumulativeDuration = 0;
360
385
  let currentPromille = 0;
@@ -369,32 +394,27 @@ function handleOutcome(params, outcome) {
369
394
  }
370
395
  if (exhausted) currentPromille = 1e5;
371
396
  if (currentPromille <= 0) {
372
- return {
373
- value: getVariant(
374
- params.definition.variants,
375
- outcome.rollFromVariant
376
- ),
377
- outcomeType: "rollout" /* ROLLOUT */
378
- };
397
+ return withVariantIndex(
398
+ getVariant(params.definition.variants, outcome.rollFromVariant),
399
+ "rollout" /* ROLLOUT */,
400
+ outcome.rollFromVariant
401
+ );
379
402
  }
380
403
  if (currentPromille >= 1e5) {
381
- return {
382
- value: getVariant(
383
- params.definition.variants,
384
- outcome.rollToVariant
385
- ),
386
- outcomeType: "rollout" /* ROLLOUT */
387
- };
404
+ return withVariantIndex(
405
+ getVariant(params.definition.variants, outcome.rollToVariant),
406
+ "rollout" /* ROLLOUT */,
407
+ outcome.rollToVariant
408
+ );
388
409
  }
389
410
  const value = hashInput(lhs, params.definition.seed);
390
411
  const threshold = currentPromille / 1e5 * UINT32_MAX;
391
- return {
392
- value: value < threshold ? getVariant(params.definition.variants, outcome.rollToVariant) : getVariant(
393
- params.definition.variants,
394
- outcome.rollFromVariant
395
- ),
396
- outcomeType: "rollout" /* ROLLOUT */
397
- };
412
+ const selectedVariantIndex = value < threshold ? outcome.rollToVariant : outcome.rollFromVariant;
413
+ return withVariantIndex(
414
+ getVariant(params.definition.variants, selectedVariantIndex),
415
+ "rollout" /* ROLLOUT */,
416
+ selectedVariantIndex
417
+ );
398
418
  }
399
419
  default: {
400
420
  const { type } = outcome;
@@ -482,7 +502,7 @@ function findWeightedIndex(weights, value, maxValue) {
482
502
  }
483
503
 
484
504
  // package.json
485
- var version = "1.5.0";
505
+ var version = "1.6.0-7d29945-20260616152249";
486
506
 
487
507
  // src/lib/report-value.ts
488
508
  function internalReportValue(key, value, data) {
@@ -521,6 +541,14 @@ function getFallbackDatafile(id) {
521
541
  if (ds.getFallbackDatafile) return ds.getFallbackDatafile();
522
542
  throw new Error("flags: This data source does not support fallbacks");
523
543
  }
544
+ function getVariantIdentifier(definition, result) {
545
+ const variantIndex = getEvaluationVariantIndex(result);
546
+ if (variantIndex === void 0) return "unknown";
547
+ return definition.variantIds?.[variantIndex] ?? String(variantIndex);
548
+ }
549
+ function trackEvaluation(controller, options) {
550
+ controller.trackEvaluation?.(options);
551
+ }
524
552
  async function evaluate2(id, flagKey, defaultValue, entities) {
525
553
  const controller = getInstance(id).controller;
526
554
  let datafile;
@@ -528,11 +556,17 @@ async function evaluate2(id, flagKey, defaultValue, entities) {
528
556
  datafile = await controller.read();
529
557
  } catch (error) {
530
558
  if (defaultValue !== void 0) {
531
- return {
559
+ const result2 = {
532
560
  value: defaultValue,
533
561
  reason: "error" /* ERROR */,
534
562
  errorMessage: error instanceof Error ? error.message : "Failed to read datafile"
535
563
  };
564
+ trackEvaluation(controller, {
565
+ flagKey,
566
+ variant: "unknown",
567
+ reason: result2.reason
568
+ });
569
+ return result2;
536
570
  }
537
571
  throw error;
538
572
  }
@@ -545,7 +579,7 @@ async function evaluate2(id, flagKey, defaultValue, entities) {
545
579
  reason: "error" /* ERROR */
546
580
  });
547
581
  }
548
- return {
582
+ const result2 = {
549
583
  value: defaultValue,
550
584
  reason: "error" /* ERROR */,
551
585
  errorCode: "FLAG_NOT_FOUND" /* FLAG_NOT_FOUND */,
@@ -559,6 +593,12 @@ async function evaluate2(id, flagKey, defaultValue, entities) {
559
593
  mode: datafile.metrics.mode
560
594
  }
561
595
  };
596
+ trackEvaluation(controller, {
597
+ flagKey,
598
+ variant: "unknown",
599
+ reason: result2.reason
600
+ });
601
+ return result2;
562
602
  }
563
603
  const evalStartTime = Date.now();
564
604
  const result = evaluate({
@@ -577,6 +617,14 @@ async function evaluate2(id, flagKey, defaultValue, entities) {
577
617
  outcomeType: result.reason !== "error" /* ERROR */ ? result.outcomeType : void 0
578
618
  });
579
619
  }
620
+ trackEvaluation(controller, {
621
+ flagKey,
622
+ variant: getVariantIdentifier(
623
+ flagDefinition,
624
+ result
625
+ ),
626
+ reason: result.reason
627
+ });
580
628
  return Object.assign(result, {
581
629
  metrics: {
582
630
  evaluationMs: evaluationDurationMs,
@@ -602,6 +650,11 @@ async function bulkEvaluate2(id, flags, entities) {
602
650
  reason: "error" /* ERROR */,
603
651
  errorMessage
604
652
  };
653
+ trackEvaluation(controller, {
654
+ flagKey: flag.key,
655
+ variant: "unknown",
656
+ reason: "error" /* ERROR */
657
+ });
605
658
  }
606
659
  return results2;
607
660
  }
@@ -633,6 +686,11 @@ async function bulkEvaluate2(id, flags, entities) {
633
686
  errorMessage: `@vercel/flags-core: Definition not found for flag "${key}"`,
634
687
  metrics: { evaluationMs: 0, ...baseMetrics }
635
688
  };
689
+ trackEvaluation(controller, {
690
+ flagKey: key,
691
+ variant: "unknown",
692
+ reason: "error" /* ERROR */
693
+ });
636
694
  continue;
637
695
  }
638
696
  toEvaluate[key] = { definition: flagDefinition, defaultValue };
@@ -654,6 +712,14 @@ async function bulkEvaluate2(id, flags, entities) {
654
712
  outcomeType: result.reason !== "error" /* ERROR */ ? result.outcomeType : void 0
655
713
  });
656
714
  }
715
+ trackEvaluation(controller, {
716
+ flagKey: key,
717
+ variant: getVariantIdentifier(
718
+ toEvaluate[key].definition,
719
+ result
720
+ ),
721
+ reason: result.reason
722
+ });
657
723
  results[key] = Object.assign(result, {
658
724
  metrics: { evaluationMs: evaluationDurationMs, ...baseMetrics }
659
725
  });
@@ -798,8 +864,8 @@ async function readBundledDefinitions(auth) {
798
864
  return { definitions: null, state: "missing-entry" };
799
865
  }
800
866
 
801
- // src/utils/usage-tracker.ts
802
- import { waitUntil } from "@vercel/functions";
867
+ // src/utils/ingest.ts
868
+ import { getVercelOidcToken } from "@vercel/oidc";
803
869
 
804
870
  // src/utils/backoff.ts
805
871
  var DEFAULT_BASE_MS = 250;
@@ -810,23 +876,72 @@ function getRetryDelayMs(attempt, options = {}) {
810
876
  const ceiling = Math.min(capMs, baseMs * 2 ** Math.max(0, attempt - 1));
811
877
  return Math.floor(Math.random() * ceiling);
812
878
  }
813
- function getJitteredWaitMs(baseMs, ratio) {
814
- const min = baseMs * (1 - ratio);
815
- const span = baseMs * 2 * ratio;
816
- return Math.floor(min + Math.random() * span);
817
- }
818
879
 
819
- // src/utils/usage-tracker.ts
820
- var RESOLVED_VOID = Promise.resolve();
880
+ // src/utils/ingest.ts
881
+ var MAX_RETRIES = 3;
882
+ var EVALUATING_OIDC_TOKEN_HEADER = "X-Vercel-Flags-Evaluating-OIDC-Token";
821
883
  var isDebugMode = process.env.DEBUG?.includes("@vercel/flags-core");
822
884
  var debugLog = (...args) => {
823
885
  if (!isDebugMode) return;
824
886
  console.log(...args);
825
887
  };
826
- var MAX_RETRIES = 3;
827
- var MAX_BATCH_SIZE = 50;
828
- var MAX_BATCH_WAIT_MS = 5e3;
829
- var BATCH_WAIT_JITTER_RATIO = 0.2;
888
+ async function getEvaluatingOidcToken(auth) {
889
+ if (!auth.sdkKey) return void 0;
890
+ try {
891
+ return await getVercelOidcToken();
892
+ } catch {
893
+ return void 0;
894
+ }
895
+ }
896
+ async function getIngestHeaders(options) {
897
+ const token = await options.auth.resolveToken();
898
+ const evaluatingOidcToken = await getEvaluatingOidcToken(options.auth);
899
+ return {
900
+ "Content-Type": "application/json",
901
+ Authorization: `Bearer ${token}`,
902
+ "User-Agent": `VercelFlagsCore/${version}`,
903
+ ...process.env.VERCEL_ENV ? { "X-Vercel-Env": process.env.VERCEL_ENV } : null,
904
+ ...evaluatingOidcToken ? { [EVALUATING_OIDC_TOKEN_HEADER]: evaluatingOidcToken } : null,
905
+ ...isDebugMode ? { "x-vercel-debug-ingest": "1" } : null
906
+ };
907
+ }
908
+ async function sendIngestEvents(options, events, flushId) {
909
+ const eventsToSend = events.map((event) => event.ingestEvent());
910
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
911
+ try {
912
+ const response = await options.fetch(`${options.host}/v1/ingest`, {
913
+ method: "POST",
914
+ headers: await getIngestHeaders(options),
915
+ body: JSON.stringify(eventsToSend)
916
+ });
917
+ debugLog(
918
+ `@vercel/flags-core: Ingest response ${response.status} for ${eventsToSend.length} events on ${response.headers.get("x-vercel-id")}`
919
+ );
920
+ if (response.ok) {
921
+ break;
922
+ }
923
+ throw new Error(
924
+ `Ingest endpoint responded with status ${response.status} for ${eventsToSend.length} events on request ${response.headers.get("x-vercel-id")}.
925
+ Response body: ${await response.text().catch(() => null)}`
926
+ );
927
+ } catch (error) {
928
+ console.error(
929
+ `@vercel/flags-core: Error sending events (attempt=${attempt}/${MAX_RETRIES} flushId=${flushId}):`,
930
+ error
931
+ );
932
+ if (attempt < MAX_RETRIES) {
933
+ const delayMs = getRetryDelayMs(attempt);
934
+ await new Promise((res) => setTimeout(res, delayMs));
935
+ } else {
936
+ console.error(
937
+ `@vercel/flags-core: Dropped ${eventsToSend.length} events after ${MAX_RETRIES} attempts (flushId=${flushId})`
938
+ );
939
+ }
940
+ }
941
+ }
942
+ }
943
+
944
+ // src/utils/request-context.ts
830
945
  var SYMBOL_FOR_REQ_CONTEXT = /* @__PURE__ */ Symbol.for("@vercel/request-context");
831
946
  var fromSymbol = globalThis;
832
947
  function getRequestContext() {
@@ -838,36 +953,227 @@ function getRequestContext() {
838
953
  headers: ctx.headers
839
954
  };
840
955
  }
841
- return { ctx, headers: void 0 };
956
+ return { ctx: void 0, headers: void 0 };
842
957
  } catch {
843
958
  return { ctx: void 0, headers: void 0 };
844
959
  }
845
960
  }
961
+
962
+ // src/utils/scheduler.ts
963
+ import { waitUntil } from "@vercel/functions";
964
+ var MAX_COUNT = 50;
965
+ var IDLE_FLUSH_WAIT_MS = 5e3;
966
+ var MAX_FLUSH_WAIT_MS = 6e4;
967
+ var Scheduler = class {
968
+ constructor(onFlush) {
969
+ this.onFlush = onFlush;
970
+ }
971
+ count = 0;
972
+ resolveWait = null;
973
+ pending = null;
974
+ idleTimeout = null;
975
+ maxTimeout = null;
976
+ increment() {
977
+ this.count += 1;
978
+ if (this.count >= MAX_COUNT) {
979
+ this.resolveScheduledFlush();
980
+ }
981
+ }
982
+ scheduleFlush() {
983
+ if (!this.pending) {
984
+ this.pending = (async () => {
985
+ await new Promise((res) => {
986
+ this.resolveWait = res;
987
+ });
988
+ this.reset();
989
+ await this.onFlush();
990
+ })();
991
+ try {
992
+ waitUntil(this.pending);
993
+ } catch {
994
+ }
995
+ this.maxTimeout = setTimeout(
996
+ () => this.resolveScheduledFlush(),
997
+ MAX_FLUSH_WAIT_MS
998
+ );
999
+ }
1000
+ this.resetIdleTimeout();
1001
+ }
1002
+ /**
1003
+ * Resolves any in-flight scheduled flush and waits for its ingest to finish.
1004
+ */
1005
+ async shutdown() {
1006
+ this.clearTimeouts();
1007
+ const pending = this.pending;
1008
+ this.resolveWait?.();
1009
+ if (pending) await pending;
1010
+ }
1011
+ resetIdleTimeout() {
1012
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
1013
+ this.idleTimeout = setTimeout(
1014
+ () => this.resolveScheduledFlush(),
1015
+ IDLE_FLUSH_WAIT_MS
1016
+ );
1017
+ }
1018
+ resolveScheduledFlush() {
1019
+ this.clearTimeouts();
1020
+ this.resolveWait?.();
1021
+ }
1022
+ reset() {
1023
+ this.pending = null;
1024
+ this.resolveWait = null;
1025
+ this.count = 0;
1026
+ this.clearTimeouts();
1027
+ }
1028
+ clearTimeouts() {
1029
+ if (this.idleTimeout) {
1030
+ clearTimeout(this.idleTimeout);
1031
+ this.idleTimeout = null;
1032
+ }
1033
+ if (this.maxTimeout) {
1034
+ clearTimeout(this.maxTimeout);
1035
+ this.maxTimeout = null;
1036
+ }
1037
+ }
1038
+ };
1039
+
1040
+ // src/utils/usage/flags-config-read.ts
1041
+ var FlagsConfigReadEvent = class {
1042
+ payload;
1043
+ constructor(headers, options) {
1044
+ this.payload = {
1045
+ deploymentId: process.env.VERCEL_DEPLOYMENT_ID,
1046
+ region: process.env.VERCEL_REGION
1047
+ };
1048
+ if (headers) {
1049
+ this.payload.vercelRequestId = headers["x-vercel-id"] ?? void 0;
1050
+ this.payload.invocationHost = headers.host ?? void 0;
1051
+ }
1052
+ if (options) {
1053
+ this.payload.configOrigin = options.configOrigin;
1054
+ if (options.cacheStatus !== void 0) {
1055
+ this.payload.cacheStatus = options.cacheStatus;
1056
+ }
1057
+ if (options.cacheAction !== void 0) {
1058
+ this.payload.cacheAction = options.cacheAction;
1059
+ }
1060
+ if (options.cacheIsFirstRead !== void 0) {
1061
+ this.payload.cacheIsFirstRead = options.cacheIsFirstRead;
1062
+ }
1063
+ if (options.cacheIsBlocking !== void 0) {
1064
+ this.payload.cacheIsBlocking = options.cacheIsBlocking;
1065
+ }
1066
+ if (options.duration !== void 0) {
1067
+ this.payload.duration = options.duration;
1068
+ }
1069
+ if (options.configUpdatedAt !== void 0) {
1070
+ this.payload.configUpdatedAt = options.configUpdatedAt;
1071
+ }
1072
+ if (options.mode !== void 0) {
1073
+ this.payload.mode = options.mode;
1074
+ }
1075
+ if (options.revision !== void 0) {
1076
+ this.payload.revision = String(options.revision);
1077
+ }
1078
+ }
1079
+ const environment = process.env.VERCEL_ENV || process.env.NODE_ENV || void 0;
1080
+ if (environment) {
1081
+ this.payload.environment = environment;
1082
+ }
1083
+ }
1084
+ ingestEvent() {
1085
+ return {
1086
+ type: "FLAGS_CONFIG_READ",
1087
+ ts: Date.now(),
1088
+ payload: this.payload
1089
+ };
1090
+ }
1091
+ };
1092
+
1093
+ // src/utils/usage/flags-evaluation.ts
1094
+ function evaluationBatchKey(options) {
1095
+ return JSON.stringify([
1096
+ options.flagKey,
1097
+ options.variant,
1098
+ options.reason,
1099
+ options.clientName ?? null
1100
+ ]);
1101
+ }
1102
+ var FlagsEvaluationEvent = class {
1103
+ payload;
1104
+ constructor(eventOptions) {
1105
+ this.payload = {
1106
+ flagKey: eventOptions.flagKey,
1107
+ variant: eventOptions.variant,
1108
+ reason: eventOptions.reason,
1109
+ count: 1
1110
+ };
1111
+ if (eventOptions.clientName) {
1112
+ this.payload.clientName = eventOptions.clientName;
1113
+ }
1114
+ Object.defineProperty(this, "batchKey", {
1115
+ value: JSON.stringify([
1116
+ this.payload.flagKey,
1117
+ this.payload.variant,
1118
+ this.payload.reason,
1119
+ this.payload.clientName ?? null
1120
+ ]),
1121
+ enumerable: false
1122
+ });
1123
+ }
1124
+ increment() {
1125
+ this.payload.count += 1;
1126
+ }
1127
+ ingestEvent() {
1128
+ return {
1129
+ type: "FLAGS_EVALUATION",
1130
+ ts: Date.now(),
1131
+ payload: this.payload
1132
+ };
1133
+ }
1134
+ };
1135
+
1136
+ // src/utils/weak-map.ts
1137
+ var BetterWeakMap = class {
1138
+ _values = [];
1139
+ _keys = [];
1140
+ set(key, value) {
1141
+ this._keys.push(key);
1142
+ this._values.push(value);
1143
+ }
1144
+ values() {
1145
+ return this._values;
1146
+ }
1147
+ has(key) {
1148
+ return this._keys.includes(key);
1149
+ }
1150
+ keys() {
1151
+ return this._keys;
1152
+ }
1153
+ clear() {
1154
+ this._values = [];
1155
+ this._keys = [];
1156
+ }
1157
+ };
1158
+
1159
+ // src/utils/usage-tracker.ts
846
1160
  var UsageTracker = class {
847
- flushCounter = 0;
1161
+ flushCount = 0;
848
1162
  options;
849
- trackedRequests = /* @__PURE__ */ new WeakSet();
850
- batcher = {
851
- events: [],
852
- resolveWait: null,
853
- pending: null
854
- };
1163
+ scheduler;
1164
+ readEvents = new BetterWeakMap();
1165
+ evaluationEvents = /* @__PURE__ */ new Map();
855
1166
  constructor(options) {
856
1167
  this.options = options;
1168
+ this.scheduler = new Scheduler(() => this.flushEvents());
857
1169
  }
858
1170
  /**
859
1171
  * Triggers an immediate flush of any pending events.
860
1172
  * Returns a promise that resolves when the flush completes.
861
1173
  */
862
- flush() {
863
- if (this.batcher.pending) {
864
- this.batcher.resolveWait?.();
865
- return this.batcher.pending;
866
- }
867
- if (this.batcher.events.length > 0) {
868
- return this.flushEvents();
869
- }
870
- return RESOLVED_VOID;
1174
+ async shutdown() {
1175
+ await this.scheduler.shutdown();
1176
+ await this.flushEvents();
871
1177
  }
872
1178
  /**
873
1179
  * Tracks a config read event. Deduplicates by request context.
@@ -876,130 +1182,51 @@ var UsageTracker = class {
876
1182
  try {
877
1183
  const { ctx, headers } = getRequestContext();
878
1184
  if (!ctx) return;
879
- if (this.trackedRequests.has(ctx)) return;
880
- this.trackedRequests.add(ctx);
881
- const event = {
882
- type: "FLAGS_CONFIG_READ",
883
- ts: Date.now(),
884
- payload: {
885
- deploymentId: process.env.VERCEL_DEPLOYMENT_ID,
886
- region: process.env.VERCEL_REGION
887
- }
888
- };
889
- if (headers) {
890
- event.payload.vercelRequestId = headers["x-vercel-id"] ?? void 0;
891
- event.payload.invocationHost = headers.host ?? void 0;
892
- }
893
- if (options) {
894
- event.payload.configOrigin = options.configOrigin;
895
- if (options.cacheStatus !== void 0) {
896
- event.payload.cacheStatus = options.cacheStatus;
897
- }
898
- if (options.cacheAction !== void 0) {
899
- event.payload.cacheAction = options.cacheAction;
900
- }
901
- if (options.cacheIsFirstRead !== void 0) {
902
- event.payload.cacheIsFirstRead = options.cacheIsFirstRead;
903
- }
904
- if (options.cacheIsBlocking !== void 0) {
905
- event.payload.cacheIsBlocking = options.cacheIsBlocking;
906
- }
907
- if (options.duration !== void 0) {
908
- event.payload.duration = options.duration;
909
- }
910
- if (options.configUpdatedAt !== void 0) {
911
- event.payload.configUpdatedAt = options.configUpdatedAt;
912
- }
913
- if (options.mode !== void 0) {
914
- event.payload.mode = options.mode;
915
- }
916
- if (options.revision !== void 0) {
917
- event.payload.revision = String(options.revision);
918
- }
919
- }
920
- const environment = process.env.VERCEL_ENV || process.env.NODE_ENV || void 0;
921
- if (environment) {
922
- event.payload.environment = environment;
1185
+ if (this.readEvents.has(ctx)) {
1186
+ return;
923
1187
  }
924
- this.batcher.events.push(event);
925
- this.scheduleFlush();
1188
+ this.readEvents.set(ctx, new FlagsConfigReadEvent(headers, options));
1189
+ this.scheduler.scheduleFlush();
1190
+ this.scheduler.increment();
926
1191
  } catch (error) {
927
1192
  console.error("@vercel/flags-core: Failed to record event:", error);
928
1193
  }
929
1194
  }
930
- scheduleFlush() {
931
- if (!this.batcher.pending) {
932
- let timeout = null;
933
- const pending = (async () => {
934
- await new Promise((res) => {
935
- this.batcher.resolveWait = res;
936
- timeout = setTimeout(
937
- res,
938
- getJitteredWaitMs(MAX_BATCH_WAIT_MS, BATCH_WAIT_JITTER_RATIO)
939
- );
940
- });
941
- this.batcher.pending = null;
942
- this.batcher.resolveWait = null;
943
- if (timeout) clearTimeout(timeout);
944
- await this.flushEvents();
945
- })();
946
- try {
947
- waitUntil(pending);
948
- } catch {
1195
+ /**
1196
+ * Tracks a flag evaluation event.
1197
+ */
1198
+ trackEvaluation(options) {
1199
+ try {
1200
+ const batchKey = evaluationBatchKey(options);
1201
+ const existingEvent = this.evaluationEvents.get(batchKey);
1202
+ if (existingEvent) {
1203
+ existingEvent.increment();
1204
+ } else {
1205
+ this.evaluationEvents.set(batchKey, new FlagsEvaluationEvent(options));
1206
+ this.scheduler.increment();
949
1207
  }
950
- this.batcher.pending = pending;
951
- }
952
- if (this.batcher.events.length >= MAX_BATCH_SIZE) {
953
- this.batcher.resolveWait?.();
1208
+ this.scheduler.scheduleFlush();
1209
+ } catch (error) {
1210
+ console.error(
1211
+ "@vercel/flags-core: Failed to record evaluation event:",
1212
+ error
1213
+ );
954
1214
  }
955
1215
  }
1216
+ /**
1217
+ * Send all events to the ingest service
1218
+ */
956
1219
  async flushEvents() {
957
- if (this.batcher.events.length === 0) return;
958
- const eventsToSend = this.batcher.events;
959
- this.batcher.events = [];
960
- const flushId = ++this.flushCounter;
961
- for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
962
- try {
963
- const token = await this.options.auth.resolveToken();
964
- const response = await this.options.fetch(
965
- `${this.options.host}/v1/ingest`,
966
- {
967
- method: "POST",
968
- headers: {
969
- "Content-Type": "application/json",
970
- Authorization: `Bearer ${token}`,
971
- "User-Agent": `VercelFlagsCore/${version}`,
972
- ...process.env.VERCEL_ENV ? { "X-Vercel-Env": process.env.VERCEL_ENV } : null,
973
- ...isDebugMode ? { "x-vercel-debug-ingest": "1" } : null
974
- },
975
- body: JSON.stringify(eventsToSend)
976
- }
977
- );
978
- debugLog(
979
- `@vercel/flags-core: Ingest response ${response.status} for ${eventsToSend.length} events on ${response.headers.get("x-vercel-id")}`
980
- );
981
- if (response.ok) {
982
- break;
983
- }
984
- throw new Error(
985
- `Ingest endpoint responded with status ${response.status} for ${eventsToSend.length} events on request ${response.headers.get("x-vercel-id")}.
986
- Response body: ${await response.text().catch(() => null)}`
987
- );
988
- } catch (error) {
989
- console.error(
990
- `@vercel/flags-core: Error sending events (attempt=${attempt}/${MAX_RETRIES} flushId=${flushId}):`,
991
- error
992
- );
993
- if (attempt < MAX_RETRIES) {
994
- const delayMs = getRetryDelayMs(attempt);
995
- await new Promise((res) => setTimeout(res, delayMs));
996
- } else {
997
- console.error(
998
- `@vercel/flags-core: Dropped ${eventsToSend.length} events after ${MAX_RETRIES} attempts (flushId=${flushId})`
999
- );
1000
- }
1001
- }
1002
- }
1220
+ const events = [
1221
+ ...this.readEvents.values(),
1222
+ ...this.evaluationEvents.values()
1223
+ ];
1224
+ if (events.length === 0) return;
1225
+ this.flushCount += 1;
1226
+ const flushId = this.flushCount;
1227
+ this.readEvents.clear();
1228
+ this.evaluationEvents.clear();
1229
+ await sendIngestEvents(this.options, events, flushId);
1003
1230
  }
1004
1231
  };
1005
1232
 
@@ -1156,7 +1383,8 @@ function normalizeOptions(options) {
1156
1383
  polling,
1157
1384
  buildStep,
1158
1385
  fetch: options.fetch ?? globalThis.fetch,
1159
- host: "https://flags.vercel.com"
1386
+ host: "https://flags.vercel.com",
1387
+ clientName: options.clientName
1160
1388
  };
1161
1389
  }
1162
1390
 
@@ -1741,7 +1969,7 @@ var Controller = class {
1741
1969
  this.pollingSource.stop();
1742
1970
  this.data = this.options.datafile ? tagData(this.options.datafile, "provided") : void 0;
1743
1971
  this.transition("shutdown");
1744
- await this.usageTracker.flush();
1972
+ await this.usageTracker.shutdown();
1745
1973
  }
1746
1974
  /**
1747
1975
  * Returns the datafile with metrics.
@@ -2119,10 +2347,20 @@ var Controller = class {
2119
2347
  }
2120
2348
  this.usageTracker.trackRead(trackOptions);
2121
2349
  }
2350
+ /**
2351
+ * Tracks a flag evaluation for usage analytics.
2352
+ */
2353
+ trackEvaluation(options) {
2354
+ if (this.unauthorized) return;
2355
+ this.usageTracker.trackEvaluation({
2356
+ ...options,
2357
+ clientName: options.clientName ?? this.options.clientName
2358
+ });
2359
+ }
2122
2360
  };
2123
2361
 
2124
2362
  // src/controller/auth.ts
2125
- import { getVercelOidcToken } from "@vercel/oidc";
2363
+ import { getVercelOidcToken as getVercelOidcToken2 } from "@vercel/oidc";
2126
2364
 
2127
2365
  // src/utils/sdk-keys.ts
2128
2366
  var SDK_KEY_REGEX = /^vf_(?:server|client)_/;
@@ -2141,7 +2379,7 @@ function parseSdkKeyFromFlagsConnectionString(text) {
2141
2379
  // src/controller/auth.ts
2142
2380
  async function getOidcToken() {
2143
2381
  try {
2144
- return await getVercelOidcToken();
2382
+ return await getVercelOidcToken2();
2145
2383
  } catch {
2146
2384
  throw new Error(
2147
2385
  [
@@ -2261,4 +2499,4 @@ export {
2261
2499
  resetDefaultFlagsClient,
2262
2500
  createClient
2263
2501
  };
2264
- //# sourceMappingURL=chunk-WTGXBSWR.js.map
2502
+ //# sourceMappingURL=chunk-PSYHUXJC.js.map