@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.
@@ -35,6 +35,22 @@ var MAX_REGEX_INPUT_LENGTH = 1e4;
35
35
  var UINT32_MAX = 4294967295;
36
36
  var SCALED_WEIGHTS = /* @__PURE__ */ Symbol("@vercel/flags-core:scaledWeights");
37
37
  var COMPILED_REGEX = /* @__PURE__ */ Symbol("@vercel/flags-core:compiledRegex");
38
+ var EVALUATION_VARIANT_INDEX = /* @__PURE__ */ Symbol(
39
+ "@vercel/flags-core:evaluationVariantIndex"
40
+ );
41
+ function withVariantIndex(value, outcomeType, variantIndex) {
42
+ return Object.defineProperty(
43
+ {
44
+ value,
45
+ outcomeType
46
+ },
47
+ EVALUATION_VARIANT_INDEX,
48
+ { value: variantIndex }
49
+ );
50
+ }
51
+ function getEvaluationVariantIndex(result) {
52
+ return result[EVALUATION_VARIANT_INDEX];
53
+ }
38
54
  function getScaledWeights(outcome) {
39
55
  const cached = outcome[SCALED_WEIGHTS];
40
56
  if (cached) return cached;
@@ -298,10 +314,11 @@ function getVariant(variants, index) {
298
314
  }
299
315
  function handleOutcome(params, outcome) {
300
316
  if (typeof outcome === "number") {
301
- return {
302
- value: getVariant(params.definition.variants, outcome),
303
- outcomeType: "value" /* VALUE */
304
- };
317
+ return withVariantIndex(
318
+ getVariant(params.definition.variants, outcome),
319
+ "value" /* VALUE */,
320
+ outcome
321
+ );
305
322
  }
306
323
  switch (outcome.type) {
307
324
  case "split": {
@@ -311,15 +328,21 @@ function handleOutcome(params, outcome) {
311
328
  outcome.defaultVariant
312
329
  );
313
330
  if (typeof lhs !== "string") {
314
- return { value: defaultOutcome, outcomeType: "split" /* SPLIT */ };
331
+ return withVariantIndex(
332
+ defaultOutcome,
333
+ "split" /* SPLIT */,
334
+ outcome.defaultVariant
335
+ );
315
336
  }
316
337
  const value = hashInput(lhs, params.definition.seed);
317
338
  const scaledWeights = getScaledWeights(outcome);
318
339
  const variantIndex = findWeightedIndex(scaledWeights, value, UINT32_MAX);
319
- return {
320
- value: variantIndex === -1 ? defaultOutcome : getVariant(params.definition.variants, variantIndex),
321
- outcomeType: "split" /* SPLIT */
322
- };
340
+ const selectedVariantIndex = variantIndex === -1 ? outcome.defaultVariant : variantIndex;
341
+ return withVariantIndex(
342
+ selectedVariantIndex === outcome.defaultVariant ? defaultOutcome : getVariant(params.definition.variants, selectedVariantIndex),
343
+ "split" /* SPLIT */,
344
+ selectedVariantIndex
345
+ );
323
346
  }
324
347
  case "rollout": {
325
348
  const lhs = access(outcome.base, params);
@@ -328,18 +351,20 @@ function handleOutcome(params, outcome) {
328
351
  outcome.defaultVariant
329
352
  );
330
353
  if (typeof lhs !== "string") {
331
- return { value: defaultOutcome, outcomeType: "rollout" /* ROLLOUT */ };
354
+ return withVariantIndex(
355
+ defaultOutcome,
356
+ "rollout" /* ROLLOUT */,
357
+ outcome.defaultVariant
358
+ );
332
359
  }
333
360
  const now = Date.now();
334
361
  const elapsed = now - outcome.startTimestamp;
335
362
  if (elapsed < 0 || outcome.slots.length === 0) {
336
- return {
337
- value: getVariant(
338
- params.definition.variants,
339
- outcome.rollFromVariant
340
- ),
341
- outcomeType: "rollout" /* ROLLOUT */
342
- };
363
+ return withVariantIndex(
364
+ getVariant(params.definition.variants, outcome.rollFromVariant),
365
+ "rollout" /* ROLLOUT */,
366
+ outcome.rollFromVariant
367
+ );
343
368
  }
344
369
  let cumulativeDuration = 0;
345
370
  let currentPromille = 0;
@@ -354,32 +379,27 @@ function handleOutcome(params, outcome) {
354
379
  }
355
380
  if (exhausted) currentPromille = 1e5;
356
381
  if (currentPromille <= 0) {
357
- return {
358
- value: getVariant(
359
- params.definition.variants,
360
- outcome.rollFromVariant
361
- ),
362
- outcomeType: "rollout" /* ROLLOUT */
363
- };
382
+ return withVariantIndex(
383
+ getVariant(params.definition.variants, outcome.rollFromVariant),
384
+ "rollout" /* ROLLOUT */,
385
+ outcome.rollFromVariant
386
+ );
364
387
  }
365
388
  if (currentPromille >= 1e5) {
366
- return {
367
- value: getVariant(
368
- params.definition.variants,
369
- outcome.rollToVariant
370
- ),
371
- outcomeType: "rollout" /* ROLLOUT */
372
- };
389
+ return withVariantIndex(
390
+ getVariant(params.definition.variants, outcome.rollToVariant),
391
+ "rollout" /* ROLLOUT */,
392
+ outcome.rollToVariant
393
+ );
373
394
  }
374
395
  const value = hashInput(lhs, params.definition.seed);
375
396
  const threshold = currentPromille / 1e5 * UINT32_MAX;
376
- return {
377
- value: value < threshold ? getVariant(params.definition.variants, outcome.rollToVariant) : getVariant(
378
- params.definition.variants,
379
- outcome.rollFromVariant
380
- ),
381
- outcomeType: "rollout" /* ROLLOUT */
382
- };
397
+ const selectedVariantIndex = value < threshold ? outcome.rollToVariant : outcome.rollFromVariant;
398
+ return withVariantIndex(
399
+ getVariant(params.definition.variants, selectedVariantIndex),
400
+ "rollout" /* ROLLOUT */,
401
+ selectedVariantIndex
402
+ );
383
403
  }
384
404
  default: {
385
405
  const { type } = outcome;
@@ -467,7 +487,7 @@ function findWeightedIndex(weights, value, maxValue) {
467
487
  }
468
488
 
469
489
  // package.json
470
- var version = "1.5.0";
490
+ var version = "1.6.0-7d29945-20260616152249";
471
491
 
472
492
  // src/lib/report-value.ts
473
493
  function internalReportValue(key, value, data) {
@@ -506,6 +526,14 @@ function getFallbackDatafile(id) {
506
526
  if (ds.getFallbackDatafile) return ds.getFallbackDatafile();
507
527
  throw new Error("flags: This data source does not support fallbacks");
508
528
  }
529
+ function getVariantIdentifier(definition, result) {
530
+ const variantIndex = getEvaluationVariantIndex(result);
531
+ if (variantIndex === void 0) return "unknown";
532
+ return definition.variantIds?.[variantIndex] ?? String(variantIndex);
533
+ }
534
+ function trackEvaluation(controller, options) {
535
+ controller.trackEvaluation?.(options);
536
+ }
509
537
  async function evaluate2(id, flagKey, defaultValue, entities) {
510
538
  const controller = getInstance(id).controller;
511
539
  let datafile;
@@ -513,11 +541,17 @@ async function evaluate2(id, flagKey, defaultValue, entities) {
513
541
  datafile = await controller.read();
514
542
  } catch (error) {
515
543
  if (defaultValue !== void 0) {
516
- return {
544
+ const result2 = {
517
545
  value: defaultValue,
518
546
  reason: "error" /* ERROR */,
519
547
  errorMessage: error instanceof Error ? error.message : "Failed to read datafile"
520
548
  };
549
+ trackEvaluation(controller, {
550
+ flagKey,
551
+ variant: "unknown",
552
+ reason: result2.reason
553
+ });
554
+ return result2;
521
555
  }
522
556
  throw error;
523
557
  }
@@ -530,7 +564,7 @@ async function evaluate2(id, flagKey, defaultValue, entities) {
530
564
  reason: "error" /* ERROR */
531
565
  });
532
566
  }
533
- return {
567
+ const result2 = {
534
568
  value: defaultValue,
535
569
  reason: "error" /* ERROR */,
536
570
  errorCode: "FLAG_NOT_FOUND" /* FLAG_NOT_FOUND */,
@@ -544,6 +578,12 @@ async function evaluate2(id, flagKey, defaultValue, entities) {
544
578
  mode: datafile.metrics.mode
545
579
  }
546
580
  };
581
+ trackEvaluation(controller, {
582
+ flagKey,
583
+ variant: "unknown",
584
+ reason: result2.reason
585
+ });
586
+ return result2;
547
587
  }
548
588
  const evalStartTime = Date.now();
549
589
  const result = evaluate({
@@ -562,6 +602,14 @@ async function evaluate2(id, flagKey, defaultValue, entities) {
562
602
  outcomeType: result.reason !== "error" /* ERROR */ ? result.outcomeType : void 0
563
603
  });
564
604
  }
605
+ trackEvaluation(controller, {
606
+ flagKey,
607
+ variant: getVariantIdentifier(
608
+ flagDefinition,
609
+ result
610
+ ),
611
+ reason: result.reason
612
+ });
565
613
  return Object.assign(result, {
566
614
  metrics: {
567
615
  evaluationMs: evaluationDurationMs,
@@ -587,6 +635,11 @@ async function bulkEvaluate2(id, flags, entities) {
587
635
  reason: "error" /* ERROR */,
588
636
  errorMessage
589
637
  };
638
+ trackEvaluation(controller, {
639
+ flagKey: flag.key,
640
+ variant: "unknown",
641
+ reason: "error" /* ERROR */
642
+ });
590
643
  }
591
644
  return results2;
592
645
  }
@@ -618,6 +671,11 @@ async function bulkEvaluate2(id, flags, entities) {
618
671
  errorMessage: `@vercel/flags-core: Definition not found for flag "${key}"`,
619
672
  metrics: { evaluationMs: 0, ...baseMetrics }
620
673
  };
674
+ trackEvaluation(controller, {
675
+ flagKey: key,
676
+ variant: "unknown",
677
+ reason: "error" /* ERROR */
678
+ });
621
679
  continue;
622
680
  }
623
681
  toEvaluate[key] = { definition: flagDefinition, defaultValue };
@@ -639,6 +697,14 @@ async function bulkEvaluate2(id, flags, entities) {
639
697
  outcomeType: result.reason !== "error" /* ERROR */ ? result.outcomeType : void 0
640
698
  });
641
699
  }
700
+ trackEvaluation(controller, {
701
+ flagKey: key,
702
+ variant: getVariantIdentifier(
703
+ toEvaluate[key].definition,
704
+ result
705
+ ),
706
+ reason: result.reason
707
+ });
642
708
  results[key] = Object.assign(result, {
643
709
  metrics: { evaluationMs: evaluationDurationMs, ...baseMetrics }
644
710
  });
@@ -783,8 +849,8 @@ async function readBundledDefinitions(auth) {
783
849
  return { definitions: null, state: "missing-entry" };
784
850
  }
785
851
 
786
- // src/utils/usage-tracker.ts
787
- import { waitUntil } from "@vercel/functions";
852
+ // src/utils/ingest.ts
853
+ import { getVercelOidcToken } from "@vercel/oidc";
788
854
 
789
855
  // src/utils/backoff.ts
790
856
  var DEFAULT_BASE_MS = 250;
@@ -795,23 +861,72 @@ function getRetryDelayMs(attempt, options = {}) {
795
861
  const ceiling = Math.min(capMs, baseMs * 2 ** Math.max(0, attempt - 1));
796
862
  return Math.floor(Math.random() * ceiling);
797
863
  }
798
- function getJitteredWaitMs(baseMs, ratio) {
799
- const min = baseMs * (1 - ratio);
800
- const span = baseMs * 2 * ratio;
801
- return Math.floor(min + Math.random() * span);
802
- }
803
864
 
804
- // src/utils/usage-tracker.ts
805
- var RESOLVED_VOID = Promise.resolve();
865
+ // src/utils/ingest.ts
866
+ var MAX_RETRIES = 3;
867
+ var EVALUATING_OIDC_TOKEN_HEADER = "X-Vercel-Flags-Evaluating-OIDC-Token";
806
868
  var isDebugMode = process.env.DEBUG?.includes("@vercel/flags-core");
807
869
  var debugLog = (...args) => {
808
870
  if (!isDebugMode) return;
809
871
  console.log(...args);
810
872
  };
811
- var MAX_RETRIES = 3;
812
- var MAX_BATCH_SIZE = 50;
813
- var MAX_BATCH_WAIT_MS = 5e3;
814
- var BATCH_WAIT_JITTER_RATIO = 0.2;
873
+ async function getEvaluatingOidcToken(auth) {
874
+ if (!auth.sdkKey) return void 0;
875
+ try {
876
+ return await getVercelOidcToken();
877
+ } catch {
878
+ return void 0;
879
+ }
880
+ }
881
+ async function getIngestHeaders(options) {
882
+ const token = await options.auth.resolveToken();
883
+ const evaluatingOidcToken = await getEvaluatingOidcToken(options.auth);
884
+ return {
885
+ "Content-Type": "application/json",
886
+ Authorization: `Bearer ${token}`,
887
+ "User-Agent": `VercelFlagsCore/${version}`,
888
+ ...process.env.VERCEL_ENV ? { "X-Vercel-Env": process.env.VERCEL_ENV } : null,
889
+ ...evaluatingOidcToken ? { [EVALUATING_OIDC_TOKEN_HEADER]: evaluatingOidcToken } : null,
890
+ ...isDebugMode ? { "x-vercel-debug-ingest": "1" } : null
891
+ };
892
+ }
893
+ async function sendIngestEvents(options, events, flushId) {
894
+ const eventsToSend = events.map((event) => event.ingestEvent());
895
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
896
+ try {
897
+ const response = await options.fetch(`${options.host}/v1/ingest`, {
898
+ method: "POST",
899
+ headers: await getIngestHeaders(options),
900
+ body: JSON.stringify(eventsToSend)
901
+ });
902
+ debugLog(
903
+ `@vercel/flags-core: Ingest response ${response.status} for ${eventsToSend.length} events on ${response.headers.get("x-vercel-id")}`
904
+ );
905
+ if (response.ok) {
906
+ break;
907
+ }
908
+ throw new Error(
909
+ `Ingest endpoint responded with status ${response.status} for ${eventsToSend.length} events on request ${response.headers.get("x-vercel-id")}.
910
+ Response body: ${await response.text().catch(() => null)}`
911
+ );
912
+ } catch (error) {
913
+ console.error(
914
+ `@vercel/flags-core: Error sending events (attempt=${attempt}/${MAX_RETRIES} flushId=${flushId}):`,
915
+ error
916
+ );
917
+ if (attempt < MAX_RETRIES) {
918
+ const delayMs = getRetryDelayMs(attempt);
919
+ await new Promise((res) => setTimeout(res, delayMs));
920
+ } else {
921
+ console.error(
922
+ `@vercel/flags-core: Dropped ${eventsToSend.length} events after ${MAX_RETRIES} attempts (flushId=${flushId})`
923
+ );
924
+ }
925
+ }
926
+ }
927
+ }
928
+
929
+ // src/utils/request-context.ts
815
930
  var SYMBOL_FOR_REQ_CONTEXT = /* @__PURE__ */ Symbol.for("@vercel/request-context");
816
931
  var fromSymbol = globalThis;
817
932
  function getRequestContext() {
@@ -823,36 +938,227 @@ function getRequestContext() {
823
938
  headers: ctx.headers
824
939
  };
825
940
  }
826
- return { ctx, headers: void 0 };
941
+ return { ctx: void 0, headers: void 0 };
827
942
  } catch {
828
943
  return { ctx: void 0, headers: void 0 };
829
944
  }
830
945
  }
946
+
947
+ // src/utils/scheduler.ts
948
+ import { waitUntil } from "@vercel/functions";
949
+ var MAX_COUNT = 50;
950
+ var IDLE_FLUSH_WAIT_MS = 5e3;
951
+ var MAX_FLUSH_WAIT_MS = 6e4;
952
+ var Scheduler = class {
953
+ constructor(onFlush) {
954
+ this.onFlush = onFlush;
955
+ }
956
+ count = 0;
957
+ resolveWait = null;
958
+ pending = null;
959
+ idleTimeout = null;
960
+ maxTimeout = null;
961
+ increment() {
962
+ this.count += 1;
963
+ if (this.count >= MAX_COUNT) {
964
+ this.resolveScheduledFlush();
965
+ }
966
+ }
967
+ scheduleFlush() {
968
+ if (!this.pending) {
969
+ this.pending = (async () => {
970
+ await new Promise((res) => {
971
+ this.resolveWait = res;
972
+ });
973
+ this.reset();
974
+ await this.onFlush();
975
+ })();
976
+ try {
977
+ waitUntil(this.pending);
978
+ } catch {
979
+ }
980
+ this.maxTimeout = setTimeout(
981
+ () => this.resolveScheduledFlush(),
982
+ MAX_FLUSH_WAIT_MS
983
+ );
984
+ }
985
+ this.resetIdleTimeout();
986
+ }
987
+ /**
988
+ * Resolves any in-flight scheduled flush and waits for its ingest to finish.
989
+ */
990
+ async shutdown() {
991
+ this.clearTimeouts();
992
+ const pending = this.pending;
993
+ this.resolveWait?.();
994
+ if (pending) await pending;
995
+ }
996
+ resetIdleTimeout() {
997
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
998
+ this.idleTimeout = setTimeout(
999
+ () => this.resolveScheduledFlush(),
1000
+ IDLE_FLUSH_WAIT_MS
1001
+ );
1002
+ }
1003
+ resolveScheduledFlush() {
1004
+ this.clearTimeouts();
1005
+ this.resolveWait?.();
1006
+ }
1007
+ reset() {
1008
+ this.pending = null;
1009
+ this.resolveWait = null;
1010
+ this.count = 0;
1011
+ this.clearTimeouts();
1012
+ }
1013
+ clearTimeouts() {
1014
+ if (this.idleTimeout) {
1015
+ clearTimeout(this.idleTimeout);
1016
+ this.idleTimeout = null;
1017
+ }
1018
+ if (this.maxTimeout) {
1019
+ clearTimeout(this.maxTimeout);
1020
+ this.maxTimeout = null;
1021
+ }
1022
+ }
1023
+ };
1024
+
1025
+ // src/utils/usage/flags-config-read.ts
1026
+ var FlagsConfigReadEvent = class {
1027
+ payload;
1028
+ constructor(headers, options) {
1029
+ this.payload = {
1030
+ deploymentId: process.env.VERCEL_DEPLOYMENT_ID,
1031
+ region: process.env.VERCEL_REGION
1032
+ };
1033
+ if (headers) {
1034
+ this.payload.vercelRequestId = headers["x-vercel-id"] ?? void 0;
1035
+ this.payload.invocationHost = headers.host ?? void 0;
1036
+ }
1037
+ if (options) {
1038
+ this.payload.configOrigin = options.configOrigin;
1039
+ if (options.cacheStatus !== void 0) {
1040
+ this.payload.cacheStatus = options.cacheStatus;
1041
+ }
1042
+ if (options.cacheAction !== void 0) {
1043
+ this.payload.cacheAction = options.cacheAction;
1044
+ }
1045
+ if (options.cacheIsFirstRead !== void 0) {
1046
+ this.payload.cacheIsFirstRead = options.cacheIsFirstRead;
1047
+ }
1048
+ if (options.cacheIsBlocking !== void 0) {
1049
+ this.payload.cacheIsBlocking = options.cacheIsBlocking;
1050
+ }
1051
+ if (options.duration !== void 0) {
1052
+ this.payload.duration = options.duration;
1053
+ }
1054
+ if (options.configUpdatedAt !== void 0) {
1055
+ this.payload.configUpdatedAt = options.configUpdatedAt;
1056
+ }
1057
+ if (options.mode !== void 0) {
1058
+ this.payload.mode = options.mode;
1059
+ }
1060
+ if (options.revision !== void 0) {
1061
+ this.payload.revision = String(options.revision);
1062
+ }
1063
+ }
1064
+ const environment = process.env.VERCEL_ENV || process.env.NODE_ENV || void 0;
1065
+ if (environment) {
1066
+ this.payload.environment = environment;
1067
+ }
1068
+ }
1069
+ ingestEvent() {
1070
+ return {
1071
+ type: "FLAGS_CONFIG_READ",
1072
+ ts: Date.now(),
1073
+ payload: this.payload
1074
+ };
1075
+ }
1076
+ };
1077
+
1078
+ // src/utils/usage/flags-evaluation.ts
1079
+ function evaluationBatchKey(options) {
1080
+ return JSON.stringify([
1081
+ options.flagKey,
1082
+ options.variant,
1083
+ options.reason,
1084
+ options.clientName ?? null
1085
+ ]);
1086
+ }
1087
+ var FlagsEvaluationEvent = class {
1088
+ payload;
1089
+ constructor(eventOptions) {
1090
+ this.payload = {
1091
+ flagKey: eventOptions.flagKey,
1092
+ variant: eventOptions.variant,
1093
+ reason: eventOptions.reason,
1094
+ count: 1
1095
+ };
1096
+ if (eventOptions.clientName) {
1097
+ this.payload.clientName = eventOptions.clientName;
1098
+ }
1099
+ Object.defineProperty(this, "batchKey", {
1100
+ value: JSON.stringify([
1101
+ this.payload.flagKey,
1102
+ this.payload.variant,
1103
+ this.payload.reason,
1104
+ this.payload.clientName ?? null
1105
+ ]),
1106
+ enumerable: false
1107
+ });
1108
+ }
1109
+ increment() {
1110
+ this.payload.count += 1;
1111
+ }
1112
+ ingestEvent() {
1113
+ return {
1114
+ type: "FLAGS_EVALUATION",
1115
+ ts: Date.now(),
1116
+ payload: this.payload
1117
+ };
1118
+ }
1119
+ };
1120
+
1121
+ // src/utils/weak-map.ts
1122
+ var BetterWeakMap = class {
1123
+ _values = [];
1124
+ _keys = [];
1125
+ set(key, value) {
1126
+ this._keys.push(key);
1127
+ this._values.push(value);
1128
+ }
1129
+ values() {
1130
+ return this._values;
1131
+ }
1132
+ has(key) {
1133
+ return this._keys.includes(key);
1134
+ }
1135
+ keys() {
1136
+ return this._keys;
1137
+ }
1138
+ clear() {
1139
+ this._values = [];
1140
+ this._keys = [];
1141
+ }
1142
+ };
1143
+
1144
+ // src/utils/usage-tracker.ts
831
1145
  var UsageTracker = class {
832
- flushCounter = 0;
1146
+ flushCount = 0;
833
1147
  options;
834
- trackedRequests = /* @__PURE__ */ new WeakSet();
835
- batcher = {
836
- events: [],
837
- resolveWait: null,
838
- pending: null
839
- };
1148
+ scheduler;
1149
+ readEvents = new BetterWeakMap();
1150
+ evaluationEvents = /* @__PURE__ */ new Map();
840
1151
  constructor(options) {
841
1152
  this.options = options;
1153
+ this.scheduler = new Scheduler(() => this.flushEvents());
842
1154
  }
843
1155
  /**
844
1156
  * Triggers an immediate flush of any pending events.
845
1157
  * Returns a promise that resolves when the flush completes.
846
1158
  */
847
- flush() {
848
- if (this.batcher.pending) {
849
- this.batcher.resolveWait?.();
850
- return this.batcher.pending;
851
- }
852
- if (this.batcher.events.length > 0) {
853
- return this.flushEvents();
854
- }
855
- return RESOLVED_VOID;
1159
+ async shutdown() {
1160
+ await this.scheduler.shutdown();
1161
+ await this.flushEvents();
856
1162
  }
857
1163
  /**
858
1164
  * Tracks a config read event. Deduplicates by request context.
@@ -861,130 +1167,51 @@ var UsageTracker = class {
861
1167
  try {
862
1168
  const { ctx, headers } = getRequestContext();
863
1169
  if (!ctx) return;
864
- if (this.trackedRequests.has(ctx)) return;
865
- this.trackedRequests.add(ctx);
866
- const event = {
867
- type: "FLAGS_CONFIG_READ",
868
- ts: Date.now(),
869
- payload: {
870
- deploymentId: process.env.VERCEL_DEPLOYMENT_ID,
871
- region: process.env.VERCEL_REGION
872
- }
873
- };
874
- if (headers) {
875
- event.payload.vercelRequestId = headers["x-vercel-id"] ?? void 0;
876
- event.payload.invocationHost = headers.host ?? void 0;
877
- }
878
- if (options) {
879
- event.payload.configOrigin = options.configOrigin;
880
- if (options.cacheStatus !== void 0) {
881
- event.payload.cacheStatus = options.cacheStatus;
882
- }
883
- if (options.cacheAction !== void 0) {
884
- event.payload.cacheAction = options.cacheAction;
885
- }
886
- if (options.cacheIsFirstRead !== void 0) {
887
- event.payload.cacheIsFirstRead = options.cacheIsFirstRead;
888
- }
889
- if (options.cacheIsBlocking !== void 0) {
890
- event.payload.cacheIsBlocking = options.cacheIsBlocking;
891
- }
892
- if (options.duration !== void 0) {
893
- event.payload.duration = options.duration;
894
- }
895
- if (options.configUpdatedAt !== void 0) {
896
- event.payload.configUpdatedAt = options.configUpdatedAt;
897
- }
898
- if (options.mode !== void 0) {
899
- event.payload.mode = options.mode;
900
- }
901
- if (options.revision !== void 0) {
902
- event.payload.revision = String(options.revision);
903
- }
904
- }
905
- const environment = process.env.VERCEL_ENV || process.env.NODE_ENV || void 0;
906
- if (environment) {
907
- event.payload.environment = environment;
1170
+ if (this.readEvents.has(ctx)) {
1171
+ return;
908
1172
  }
909
- this.batcher.events.push(event);
910
- this.scheduleFlush();
1173
+ this.readEvents.set(ctx, new FlagsConfigReadEvent(headers, options));
1174
+ this.scheduler.scheduleFlush();
1175
+ this.scheduler.increment();
911
1176
  } catch (error) {
912
1177
  console.error("@vercel/flags-core: Failed to record event:", error);
913
1178
  }
914
1179
  }
915
- scheduleFlush() {
916
- if (!this.batcher.pending) {
917
- let timeout = null;
918
- const pending = (async () => {
919
- await new Promise((res) => {
920
- this.batcher.resolveWait = res;
921
- timeout = setTimeout(
922
- res,
923
- getJitteredWaitMs(MAX_BATCH_WAIT_MS, BATCH_WAIT_JITTER_RATIO)
924
- );
925
- });
926
- this.batcher.pending = null;
927
- this.batcher.resolveWait = null;
928
- if (timeout) clearTimeout(timeout);
929
- await this.flushEvents();
930
- })();
931
- try {
932
- waitUntil(pending);
933
- } catch {
1180
+ /**
1181
+ * Tracks a flag evaluation event.
1182
+ */
1183
+ trackEvaluation(options) {
1184
+ try {
1185
+ const batchKey = evaluationBatchKey(options);
1186
+ const existingEvent = this.evaluationEvents.get(batchKey);
1187
+ if (existingEvent) {
1188
+ existingEvent.increment();
1189
+ } else {
1190
+ this.evaluationEvents.set(batchKey, new FlagsEvaluationEvent(options));
1191
+ this.scheduler.increment();
934
1192
  }
935
- this.batcher.pending = pending;
936
- }
937
- if (this.batcher.events.length >= MAX_BATCH_SIZE) {
938
- this.batcher.resolveWait?.();
1193
+ this.scheduler.scheduleFlush();
1194
+ } catch (error) {
1195
+ console.error(
1196
+ "@vercel/flags-core: Failed to record evaluation event:",
1197
+ error
1198
+ );
939
1199
  }
940
1200
  }
1201
+ /**
1202
+ * Send all events to the ingest service
1203
+ */
941
1204
  async flushEvents() {
942
- if (this.batcher.events.length === 0) return;
943
- const eventsToSend = this.batcher.events;
944
- this.batcher.events = [];
945
- const flushId = ++this.flushCounter;
946
- for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
947
- try {
948
- const token = await this.options.auth.resolveToken();
949
- const response = await this.options.fetch(
950
- `${this.options.host}/v1/ingest`,
951
- {
952
- method: "POST",
953
- headers: {
954
- "Content-Type": "application/json",
955
- Authorization: `Bearer ${token}`,
956
- "User-Agent": `VercelFlagsCore/${version}`,
957
- ...process.env.VERCEL_ENV ? { "X-Vercel-Env": process.env.VERCEL_ENV } : null,
958
- ...isDebugMode ? { "x-vercel-debug-ingest": "1" } : null
959
- },
960
- body: JSON.stringify(eventsToSend)
961
- }
962
- );
963
- debugLog(
964
- `@vercel/flags-core: Ingest response ${response.status} for ${eventsToSend.length} events on ${response.headers.get("x-vercel-id")}`
965
- );
966
- if (response.ok) {
967
- break;
968
- }
969
- throw new Error(
970
- `Ingest endpoint responded with status ${response.status} for ${eventsToSend.length} events on request ${response.headers.get("x-vercel-id")}.
971
- Response body: ${await response.text().catch(() => null)}`
972
- );
973
- } catch (error) {
974
- console.error(
975
- `@vercel/flags-core: Error sending events (attempt=${attempt}/${MAX_RETRIES} flushId=${flushId}):`,
976
- error
977
- );
978
- if (attempt < MAX_RETRIES) {
979
- const delayMs = getRetryDelayMs(attempt);
980
- await new Promise((res) => setTimeout(res, delayMs));
981
- } else {
982
- console.error(
983
- `@vercel/flags-core: Dropped ${eventsToSend.length} events after ${MAX_RETRIES} attempts (flushId=${flushId})`
984
- );
985
- }
986
- }
987
- }
1205
+ const events = [
1206
+ ...this.readEvents.values(),
1207
+ ...this.evaluationEvents.values()
1208
+ ];
1209
+ if (events.length === 0) return;
1210
+ this.flushCount += 1;
1211
+ const flushId = this.flushCount;
1212
+ this.readEvents.clear();
1213
+ this.evaluationEvents.clear();
1214
+ await sendIngestEvents(this.options, events, flushId);
988
1215
  }
989
1216
  };
990
1217
 
@@ -1141,7 +1368,8 @@ function normalizeOptions(options) {
1141
1368
  polling,
1142
1369
  buildStep,
1143
1370
  fetch: options.fetch ?? globalThis.fetch,
1144
- host: "https://flags.vercel.com"
1371
+ host: "https://flags.vercel.com",
1372
+ clientName: options.clientName
1145
1373
  };
1146
1374
  }
1147
1375
 
@@ -1726,7 +1954,7 @@ var Controller = class {
1726
1954
  this.pollingSource.stop();
1727
1955
  this.data = this.options.datafile ? tagData(this.options.datafile, "provided") : void 0;
1728
1956
  this.transition("shutdown");
1729
- await this.usageTracker.flush();
1957
+ await this.usageTracker.shutdown();
1730
1958
  }
1731
1959
  /**
1732
1960
  * Returns the datafile with metrics.
@@ -2104,10 +2332,20 @@ var Controller = class {
2104
2332
  }
2105
2333
  this.usageTracker.trackRead(trackOptions);
2106
2334
  }
2335
+ /**
2336
+ * Tracks a flag evaluation for usage analytics.
2337
+ */
2338
+ trackEvaluation(options) {
2339
+ if (this.unauthorized) return;
2340
+ this.usageTracker.trackEvaluation({
2341
+ ...options,
2342
+ clientName: options.clientName ?? this.options.clientName
2343
+ });
2344
+ }
2107
2345
  };
2108
2346
 
2109
2347
  // src/controller/auth.ts
2110
- import { getVercelOidcToken } from "@vercel/oidc";
2348
+ import { getVercelOidcToken as getVercelOidcToken2 } from "@vercel/oidc";
2111
2349
 
2112
2350
  // src/utils/sdk-keys.ts
2113
2351
  var SDK_KEY_REGEX = /^vf_(?:server|client)_/;
@@ -2126,7 +2364,7 @@ function parseSdkKeyFromFlagsConnectionString(text) {
2126
2364
  // src/controller/auth.ts
2127
2365
  async function getOidcToken() {
2128
2366
  try {
2129
- return await getVercelOidcToken();
2367
+ return await getVercelOidcToken2();
2130
2368
  } catch {
2131
2369
  throw new Error(
2132
2370
  [
@@ -2271,4 +2509,4 @@ export {
2271
2509
  resetDefaultFlagsClient,
2272
2510
  createClient
2273
2511
  };
2274
- //# sourceMappingURL=chunk-D3AZORK2.js.map
2512
+ //# sourceMappingURL=chunk-S62BO7JU.js.map