@vercel/flags-core 1.6.0-fa24822-20260629103652 → 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/CHANGELOG.md CHANGED
@@ -1,10 +1,25 @@
1
1
  # @vercel/flags-core
2
2
 
3
- ## 1.6.0-fa24822-20260629103652
3
+ ## 1.6.0
4
4
 
5
5
  ### Minor Changes
6
6
 
7
- - [#401](https://github.com/vercel/flags/pull/401) [`1f8b03f`](https://github.com/vercel/flags/commit/1f8b03f2be1624391277322f21d53352017bd6a7) Thanks [@luismeyer](https://github.com/luismeyer)! - Add aggregated flag evaluation telemetry and a `clientName` option for the Vercel Flags client.
7
+ - [#401](https://github.com/vercel/flags/pull/401) [`9dff590`](https://github.com/vercel/flags/commit/9dff590bd5628bd93098637c2e9b3d1a043e4d4b) Thanks [@luismeyer](https://github.com/luismeyer)! - Add aggregated flag evaluation telemetry and a `clientName` option for the Vercel Flags client.
8
+
9
+ ## 1.5.2
10
+
11
+ ### Patch Changes
12
+
13
+ - [#416](https://github.com/vercel/flags/pull/416) [`f60c99d`](https://github.com/vercel/flags/commit/f60c99d70741e5e8e5af0a069deaf34a3129a27e) Thanks [@dferber90](https://github.com/dferber90)! - Fix datafile serialization across the RSC server/client boundary.
14
+
15
+ Evaluation memoized scaled split weights and compiled regexes by attaching
16
+ symbol-keyed properties directly onto objects inside the datafile. While
17
+ symbols are invisible to `JSON.stringify`, React Server Components serialization
18
+ walks objects directly and chokes on these (notably the non-serializable
19
+ `RegExp`), so datafiles could no longer be passed from server to client
20
+ components. Memoization now uses module-level `WeakMap`s keyed by the
21
+ outcome/rhs objects, leaving datafile objects pristine while keeping identical
22
+ caching semantics and lifetime.
8
23
 
9
24
  ## 1.5.1
10
25
 
@@ -48,21 +48,21 @@ var Packed;
48
48
  // src/evaluate.ts
49
49
  var MAX_REGEX_INPUT_LENGTH = 1e4;
50
50
  var UINT32_MAX = 4294967295;
51
- var SCALED_WEIGHTS = /* @__PURE__ */ Symbol("@vercel/flags-core:scaledWeights");
52
- var COMPILED_REGEX = /* @__PURE__ */ Symbol("@vercel/flags-core:compiledRegex");
51
+ var scaledWeightsCache = /* @__PURE__ */ new WeakMap();
52
+ var compiledRegexCache = /* @__PURE__ */ new WeakMap();
53
53
  function getScaledWeights(outcome) {
54
- const cached = outcome[SCALED_WEIGHTS];
54
+ const cached = scaledWeightsCache.get(outcome);
55
55
  if (cached) return cached;
56
56
  const total = sum(outcome.weights);
57
57
  const scaled = outcome.weights.map((w) => w / total * UINT32_MAX);
58
- outcome[SCALED_WEIGHTS] = scaled;
58
+ scaledWeightsCache.set(outcome, scaled);
59
59
  return scaled;
60
60
  }
61
61
  function getCompiledRegex(rhs) {
62
- const cached = rhs[COMPILED_REGEX];
62
+ const cached = compiledRegexCache.get(rhs);
63
63
  if (cached) return cached;
64
64
  const compiled = new RegExp(rhs.pattern, rhs.flags);
65
- rhs[COMPILED_REGEX] = compiled;
65
+ compiledRegexCache.set(rhs, compiled);
66
66
  return compiled;
67
67
  }
68
68
  function exhaustivenessCheck(_) {
@@ -494,7 +494,7 @@ function findWeightedIndex(weights, value, maxValue) {
494
494
  }
495
495
 
496
496
  // package.json
497
- var version = "1.6.0-fa24822-20260629103652";
497
+ var version = "1.6.0";
498
498
 
499
499
  // src/lib/report-value.ts
500
500
  function internalReportValue(key, value, data) {
@@ -869,7 +869,9 @@ function getJitteredWaitMs(baseMs, ratio) {
869
869
 
870
870
  // src/utils/ingest.ts
871
871
  var MAX_RETRIES = 3;
872
+ var MAX_EVENTS_PER_REQUEST = 2e3;
872
873
  var EVALUATING_OIDC_TOKEN_HEADER = "X-Vercel-Flags-OIDC-Token";
874
+ var FLUSH_REASON_HEADER = "X-Vercel-Flags-Flush-Reason";
873
875
  var isDebugMode = process.env.DEBUG?.includes("@vercel/flags-core");
874
876
  var debugLog = (...args) => {
875
877
  if (!isDebugMode) return;
@@ -883,25 +885,36 @@ async function getEvaluatingOidcToken(auth) {
883
885
  return void 0;
884
886
  }
885
887
  }
886
- async function getIngestHeaders(options) {
888
+ async function getIngestHeaders(options, flushReason) {
887
889
  const token = await options.auth.resolveToken();
888
890
  const evaluatingOidcToken = await getEvaluatingOidcToken(options.auth);
889
891
  return {
890
892
  "Content-Type": "application/json",
891
893
  Authorization: `Bearer ${token}`,
892
894
  "User-Agent": `VercelFlagsCore/${version}`,
895
+ [FLUSH_REASON_HEADER]: flushReason,
893
896
  ...process.env.VERCEL_ENV ? { "X-Vercel-Env": process.env.VERCEL_ENV } : null,
894
897
  ...evaluatingOidcToken ? { [EVALUATING_OIDC_TOKEN_HEADER]: evaluatingOidcToken } : null,
895
898
  ...isDebugMode ? { "x-vercel-debug-ingest": "1" } : null
896
899
  };
897
900
  }
898
- async function sendIngestEvents(options, events, flushId) {
901
+ async function sendIngestEvents(options, events, flushId, flushReason) {
899
902
  const eventsToSend = events.map((event) => event.ingestEvent());
903
+ for (let i = 0; i < eventsToSend.length; i += MAX_EVENTS_PER_REQUEST) {
904
+ await sendIngestChunk(
905
+ options,
906
+ eventsToSend.slice(i, i + MAX_EVENTS_PER_REQUEST),
907
+ flushId,
908
+ flushReason
909
+ );
910
+ }
911
+ }
912
+ async function sendIngestChunk(options, eventsToSend, flushId, flushReason) {
900
913
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
901
914
  try {
902
915
  const response = await options.fetch(`${options.host}/v1/ingest`, {
903
916
  method: "POST",
904
- headers: await getIngestHeaders(options),
917
+ headers: await getIngestHeaders(options, flushReason),
905
918
  body: JSON.stringify(eventsToSend)
906
919
  });
907
920
  debugLog(
@@ -951,7 +964,6 @@ function getRequestContext() {
951
964
 
952
965
  // src/utils/scheduler.ts
953
966
  import { waitUntil } from "@vercel/functions";
954
- var MAX_COUNT = 50;
955
967
  var IDLE_FLUSH_WAIT_MS = 5e3;
956
968
  var IDLE_FLUSH_JITTER_RATIO = 0.2;
957
969
  var MAX_FLUSH_WAIT_MS = 6e4;
@@ -959,32 +971,25 @@ var Scheduler = class {
959
971
  constructor(onFlush) {
960
972
  this.onFlush = onFlush;
961
973
  }
962
- count = 0;
963
974
  resolveWait = null;
964
975
  pending = null;
965
976
  idleTimeout = null;
966
977
  maxTimeout = null;
967
- increment() {
968
- this.count += 1;
969
- if (this.count >= MAX_COUNT) {
970
- this.resolveScheduledFlush();
971
- }
972
- }
973
978
  scheduleFlush() {
974
979
  if (!this.pending) {
975
980
  this.pending = (async () => {
976
- await new Promise((res) => {
981
+ const reason = await new Promise((res) => {
977
982
  this.resolveWait = res;
978
983
  });
979
984
  this.reset();
980
- await this.onFlush();
985
+ await this.onFlush(reason);
981
986
  })();
982
987
  try {
983
988
  waitUntil(this.pending);
984
989
  } catch {
985
990
  }
986
991
  this.maxTimeout = setTimeout(
987
- () => this.resolveScheduledFlush(),
992
+ () => this.resolveScheduledFlush("max_timeout"),
988
993
  MAX_FLUSH_WAIT_MS
989
994
  );
990
995
  }
@@ -996,24 +1001,23 @@ var Scheduler = class {
996
1001
  async shutdown() {
997
1002
  this.clearTimeouts();
998
1003
  const pending = this.pending;
999
- this.resolveWait?.();
1004
+ this.resolveScheduledFlush("shutdown");
1000
1005
  if (pending) await pending;
1001
1006
  }
1002
1007
  resetIdleTimeout() {
1003
1008
  if (this.idleTimeout) clearTimeout(this.idleTimeout);
1004
1009
  this.idleTimeout = setTimeout(
1005
- () => this.resolveScheduledFlush(),
1010
+ () => this.resolveScheduledFlush("idle_timeout"),
1006
1011
  getJitteredWaitMs(IDLE_FLUSH_WAIT_MS, IDLE_FLUSH_JITTER_RATIO)
1007
1012
  );
1008
1013
  }
1009
- resolveScheduledFlush() {
1014
+ resolveScheduledFlush(reason) {
1010
1015
  this.clearTimeouts();
1011
- this.resolveWait?.();
1016
+ this.resolveWait?.(reason);
1012
1017
  }
1013
1018
  reset() {
1014
1019
  this.pending = null;
1015
1020
  this.resolveWait = null;
1016
- this.count = 0;
1017
1021
  this.clearTimeouts();
1018
1022
  }
1019
1023
  clearTimeouts() {
@@ -1124,10 +1128,6 @@ var FlagsEvaluationEvent = class {
1124
1128
  };
1125
1129
 
1126
1130
  // src/utils/usage-tracker.ts
1127
- function evaluationsDisabled() {
1128
- const value = process.env.VERCEL_FLAGS_ENABLE_METRICS?.toLowerCase();
1129
- return value !== "true";
1130
- }
1131
1131
  var UsageTracker = class {
1132
1132
  flushCount = 0;
1133
1133
  options;
@@ -1137,7 +1137,7 @@ var UsageTracker = class {
1137
1137
  evaluationEvents = /* @__PURE__ */ new Map();
1138
1138
  constructor(options) {
1139
1139
  this.options = options;
1140
- this.scheduler = new Scheduler(() => this.flushEvents());
1140
+ this.scheduler = new Scheduler((reason) => this.flushEvents(reason));
1141
1141
  }
1142
1142
  /**
1143
1143
  * Triggers an immediate flush of any pending events.
@@ -1145,7 +1145,7 @@ var UsageTracker = class {
1145
1145
  */
1146
1146
  async shutdown() {
1147
1147
  await this.scheduler.shutdown();
1148
- await this.flushEvents();
1148
+ await this.flushEvents("shutdown");
1149
1149
  }
1150
1150
  /**
1151
1151
  * Tracks a config read event. Deduplicates by request context.
@@ -1158,7 +1158,6 @@ var UsageTracker = class {
1158
1158
  this.trackedRequests.add(ctx);
1159
1159
  this.readEvents.push(new FlagsConfigReadEvent(headers, options));
1160
1160
  this.scheduler.scheduleFlush();
1161
- this.scheduler.increment();
1162
1161
  } catch (error) {
1163
1162
  console.error("@vercel/flags-core: Failed to record event:", error);
1164
1163
  }
@@ -1168,9 +1167,6 @@ var UsageTracker = class {
1168
1167
  */
1169
1168
  trackEvaluation(options) {
1170
1169
  try {
1171
- if (evaluationsDisabled()) {
1172
- return;
1173
- }
1174
1170
  const bucketedOptions = {
1175
1171
  ...options,
1176
1172
  bucketTs: minuteBucketTs()
@@ -1184,7 +1180,6 @@ var UsageTracker = class {
1184
1180
  batchKey,
1185
1181
  new FlagsEvaluationEvent(bucketedOptions)
1186
1182
  );
1187
- this.scheduler.increment();
1188
1183
  }
1189
1184
  this.scheduler.scheduleFlush();
1190
1185
  } catch (error) {
@@ -1197,14 +1192,14 @@ var UsageTracker = class {
1197
1192
  /**
1198
1193
  * Send all events to the ingest service
1199
1194
  */
1200
- async flushEvents() {
1195
+ async flushEvents(flushReason) {
1201
1196
  const events = [...this.readEvents, ...this.evaluationEvents.values()];
1202
1197
  if (events.length === 0) return;
1203
1198
  this.flushCount += 1;
1204
1199
  const flushId = this.flushCount;
1205
1200
  this.readEvents = [];
1206
1201
  this.evaluationEvents.clear();
1207
- await sendIngestEvents(this.options, events, flushId);
1202
+ await sendIngestEvents(this.options, events, flushId, flushReason);
1208
1203
  }
1209
1204
  };
1210
1205
 
@@ -1362,7 +1357,8 @@ function normalizeOptions(options) {
1362
1357
  buildStep,
1363
1358
  fetch: options.fetch ?? globalThis.fetch,
1364
1359
  host: "https://flags.vercel.com",
1365
- clientName: options.clientName
1360
+ clientName: options.clientName,
1361
+ disableMetrics: options.disableMetrics ?? false
1366
1362
  };
1367
1363
  }
1368
1364
 
@@ -2333,7 +2329,7 @@ var Controller = class {
2333
2329
  * Tracks a flag evaluation for usage analytics.
2334
2330
  */
2335
2331
  trackEvaluation(options) {
2336
- if (this.unauthorized) return;
2332
+ if (this.unauthorized || this.options.disableMetrics) return;
2337
2333
  this.usageTracker.trackEvaluation({
2338
2334
  ...options,
2339
2335
  clientName: options.clientName ?? this.options.clientName
@@ -2484,4 +2480,4 @@ export {
2484
2480
  resetDefaultFlagsClient,
2485
2481
  createClient
2486
2482
  };
2487
- //# sourceMappingURL=chunk-75KYNTIB.js.map
2483
+ //# sourceMappingURL=chunk-FBTDSIHP.js.map