@vercel/flags-core 1.8.0 → 1.8.1-27681ef-20260909081650

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,5 +1,29 @@
1
1
  # @vercel/flags-core
2
2
 
3
+ ## 1.8.1-27681ef-20260909081650
4
+
5
+ ### Patch Changes
6
+
7
+ - [#486](https://github.com/vercel/flags/pull/486) [`c9d2811`](https://github.com/vercel/flags/commit/c9d28116ebca661f4e3c73f301d4b2d35310c823) Thanks [@dferber90](https://github.com/dferber90)! - Add APIs for reporting flag exposures and override values.
8
+
9
+ - The `experimental_reportExposures` client option for supplying an exposure
10
+ reporter.
11
+ - The `experimental_reportOverride` client method for reporting values set by
12
+ the Flags SDK override cookie.
13
+ - The `experimental_exposureLogging` option on `evaluate()` and
14
+ `bulkEvaluate()` for disabling exposure reporting for an individual call.
15
+ - Experiment assignment metadata on `EvaluationResult.experiment`.
16
+ - The `experimental_EvaluationOptions`,
17
+ `experimental_ExperimentAssignment`, `experimental_Exposure`, and
18
+ `experimental_ReportExposures` types.
19
+
20
+ These APIs are not supported for general use yet. Do not use them unless
21
+ Vercel has explicitly enabled them for you.
22
+
23
+ - [#494](https://github.com/vercel/flags/pull/494) [`873d051`](https://github.com/vercel/flags/commit/873d05196e9e0fa5c251f745cb67c067ab890337) Thanks [@luismeyer](https://github.com/luismeyer)! - Request an uncompressed `/v1/stream` body when running on Bun.
24
+
25
+ Bun's `fetch` negotiates brotli or gzip by default, but its streaming decoder withholds small decoded output until more compressed input arrives. The stream's first datafile is followed by silence until the next ping, so on Bun the initial datafile never surfaced, init timed out, and every flag fell back to its default. Sending `Accept-Encoding: identity` on Bun avoids the decoder entirely; other runtimes are unchanged.
26
+
3
27
  ## 1.8.0
4
28
 
5
29
  ### Minor Changes
package/README.md CHANGED
@@ -24,6 +24,8 @@ const result = await client.evaluate<boolean>('show-new-feature', false, {
24
24
  });
25
25
  ```
26
26
 
27
+ ## Evaluation Metrics
28
+
27
29
  To associate evaluation metrics with an environment, pass the
28
30
  `metricEnvironment` option:
29
31
 
@@ -312,7 +312,43 @@ function getVariant(definition, index) {
312
312
  variantId
313
313
  };
314
314
  }
315
- function handleOutcome(params, outcome) {
315
+ function getWeightedVariantIndex(params, assignment, seed) {
316
+ const lhs = access(assignment.base, params);
317
+ if (typeof lhs !== "string") return assignment.defaultVariant;
318
+ const bucket = hashInput(lhs, seed);
319
+ const boundaries = getSplitBoundaries(assignment);
320
+ for (let index = 0; index < boundaries.length; index++) {
321
+ if (bucket < boundaries[index]) return index;
322
+ }
323
+ return assignment.defaultVariant;
324
+ }
325
+ function experimentAssignment(experiment, variantId, assignmentReason) {
326
+ if (variantId === null) return void 0;
327
+ return {
328
+ id: experiment.id,
329
+ variantId,
330
+ base: experiment.base,
331
+ rampId: experiment.rampId,
332
+ rampPercentage: experiment.rampPercentage,
333
+ assignmentReason
334
+ };
335
+ }
336
+ function outcomeAssignmentReason(outcome) {
337
+ if (typeof outcome === "number") return "variant";
338
+ switch (outcome.type) {
339
+ case "experiment":
340
+ return "experiment";
341
+ case "split":
342
+ return "split";
343
+ case "rollout":
344
+ return "rollout";
345
+ default: {
346
+ const { type } = outcome;
347
+ return exhaustivenessCheck(type);
348
+ }
349
+ }
350
+ }
351
+ function resolveOutcome(params, outcome) {
316
352
  if (typeof outcome === "number") {
317
353
  const variant = getVariant(params.definition, outcome);
318
354
  return {
@@ -322,32 +358,48 @@ function handleOutcome(params, outcome) {
322
358
  }
323
359
  switch (outcome.type) {
324
360
  case "split": {
325
- const lhs = access(outcome.base, params);
326
- const defaultOutcome = getVariant(
327
- params.definition,
328
- outcome.defaultVariant
361
+ const index = getWeightedVariantIndex(
362
+ params,
363
+ outcome,
364
+ params.definition.seed
329
365
  );
330
- if (typeof lhs !== "string") {
331
- return {
332
- ...defaultOutcome,
333
- outcomeType: "split" /* SPLIT */
334
- };
335
- }
336
- const bucket = hashInput(lhs, params.definition.seed);
337
- const boundaries = getSplitBoundaries(outcome);
338
- for (let index = 0; index < boundaries.length; index++) {
339
- if (bucket < boundaries[index]) {
340
- return {
341
- ...getVariant(params.definition, index),
342
- outcomeType: "split" /* SPLIT */
343
- };
344
- }
345
- }
346
366
  return {
347
- ...defaultOutcome,
367
+ ...getVariant(params.definition, index),
348
368
  outcomeType: "split" /* SPLIT */
349
369
  };
350
370
  }
371
+ case "experiment": {
372
+ const experiment = params.definition.experiment;
373
+ if (!experiment) {
374
+ throw new Error("@vercel/flags-core: Experiment not found");
375
+ }
376
+ const unitValue = access(experiment.base, params);
377
+ const defaultVariant = getVariant(
378
+ params.definition,
379
+ experiment.defaultVariant
380
+ );
381
+ const assignment = (variant, assignmentReason) => ({
382
+ ...variant,
383
+ outcomeType: "experiment" /* EXPERIMENT */,
384
+ experiment: experimentAssignment(
385
+ experiment,
386
+ variant.variantId,
387
+ assignmentReason
388
+ )
389
+ });
390
+ if (typeof unitValue !== "string") {
391
+ return assignment(defaultVariant, "not-enrolled");
392
+ }
393
+ const rampPercentage = experiment.rampPercentage ?? 100;
394
+ const enrolled = rampPercentage >= 100 || rampPercentage > 0 && hashInput(unitValue, experiment.enrollmentSeed) < boundaryFor(rampPercentage, 100);
395
+ if (!enrolled) return assignment(defaultVariant, "not-enrolled");
396
+ const index = getWeightedVariantIndex(
397
+ params,
398
+ experiment,
399
+ params.definition.seed
400
+ );
401
+ return assignment(getVariant(params.definition, index), "experiment");
402
+ }
351
403
  case "rollout": {
352
404
  const lhs = access(outcome.base, params);
353
405
  const defaultOutcome = getVariant(
@@ -420,6 +472,19 @@ function handleOutcome(params, outcome) {
420
472
  }
421
473
  }
422
474
  }
475
+ function handleOutcome(params, outcome, assignmentReason) {
476
+ const result = resolveOutcome(params, outcome);
477
+ const experiment = params.definition.experiment;
478
+ if (!experiment || result.experiment) return result;
479
+ return {
480
+ ...result,
481
+ experiment: experimentAssignment(
482
+ experiment,
483
+ result.variantId,
484
+ assignmentReason ?? outcomeAssignmentReason(outcome)
485
+ )
486
+ };
487
+ }
423
488
  function evaluate(params, _visited) {
424
489
  const envConfig = params.definition.environments[params.environment];
425
490
  if (typeof envConfig === "number") {
@@ -459,7 +524,7 @@ function evaluate(params, _visited) {
459
524
  (targetList) => matchTargetList(targetList, params)
460
525
  );
461
526
  if (matchedIndex > -1) {
462
- return Object.assign(handleOutcome(params, matchedIndex), {
527
+ return Object.assign(handleOutcome(params, matchedIndex, "targeted"), {
463
528
  reason: "target_match" /* TARGET_MATCH */
464
529
  });
465
530
  }
@@ -493,7 +558,7 @@ function bulkEvaluate(flags, shared) {
493
558
  }
494
559
 
495
560
  // package.json
496
- var version = "1.8.0";
561
+ var version = "1.8.1-27681ef-20260909081650";
497
562
 
498
563
  // src/lib/report-value.ts
499
564
  function internalReportValue(key, value, data) {
@@ -712,6 +777,8 @@ async function bulkEvaluate2(id, flags, entities) {
712
777
  }
713
778
 
714
779
  // src/create-raw-client.ts
780
+ import { waitUntil } from "@vercel/functions";
781
+ import { dequal } from "dequal/lite";
715
782
  var idCount = 0;
716
783
  async function performInitialize(instance, initFn) {
717
784
  try {
@@ -725,7 +792,8 @@ async function performInitialize(instance, initFn) {
725
792
  function createCreateRawClient(fns) {
726
793
  return function createRawClient({
727
794
  controller,
728
- origin
795
+ origin,
796
+ experimental_reportExposures
729
797
  }) {
730
798
  const id = idCount++;
731
799
  controllerInstanceMap.set(id, {
@@ -733,6 +801,35 @@ function createCreateRawClient(fns) {
733
801
  initialized: false,
734
802
  initPromise: null
735
803
  });
804
+ function report(exposures, entity) {
805
+ if (!experimental_reportExposures || exposures.length === 0) return;
806
+ const pending = (async () => {
807
+ try {
808
+ await experimental_reportExposures(exposures, entity);
809
+ } catch (error) {
810
+ console.error(
811
+ "@vercel/flags-core: Failed to report experiment exposures",
812
+ error
813
+ );
814
+ }
815
+ })();
816
+ try {
817
+ waitUntil(pending);
818
+ } catch {
819
+ }
820
+ }
821
+ function getExposure(flagKey, result) {
822
+ if (!result.experiment) return null;
823
+ return {
824
+ flagKey,
825
+ experimentId: result.experiment.id,
826
+ variantId: result.experiment.variantId,
827
+ base: result.experiment.base,
828
+ ...result.experiment.rampId === void 0 ? {} : { rampId: result.experiment.rampId },
829
+ ...result.experiment.rampPercentage === void 0 ? {} : { rampPercentage: result.experiment.rampPercentage },
830
+ assignmentReason: result.experiment.assignmentReason
831
+ };
832
+ }
736
833
  const api = {
737
834
  origin,
738
835
  initialize: async () => {
@@ -767,7 +864,7 @@ function createCreateRawClient(fns) {
767
864
  getFallbackDatafile: () => {
768
865
  return fns.getFallbackDatafile(id);
769
866
  },
770
- evaluate: async (flagKey, defaultValue, entities) => {
867
+ evaluate: async (flagKey, defaultValue, entities, options) => {
771
868
  const instance = controllerInstanceMap.get(id);
772
869
  if (!instance?.initialized) {
773
870
  try {
@@ -775,9 +872,22 @@ function createCreateRawClient(fns) {
775
872
  } catch {
776
873
  }
777
874
  }
778
- return fns.evaluate(id, flagKey, defaultValue, entities);
875
+ const entity = entities ?? {};
876
+ const result = await fns.evaluate(
877
+ id,
878
+ flagKey,
879
+ defaultValue,
880
+ entity
881
+ );
882
+ if (experimental_reportExposures && options?.experimental_exposureLogging !== false) {
883
+ const exposure = getExposure(flagKey, result);
884
+ if (exposure) {
885
+ report([exposure], entity);
886
+ }
887
+ }
888
+ return result;
779
889
  },
780
- bulkEvaluate: async (flags, entities) => {
890
+ bulkEvaluate: async (flags, entities, options) => {
781
891
  const instance = controllerInstanceMap.get(id);
782
892
  if (!instance?.initialized) {
783
893
  try {
@@ -785,7 +895,61 @@ function createCreateRawClient(fns) {
785
895
  } catch {
786
896
  }
787
897
  }
788
- return fns.bulkEvaluate(id, flags, entities);
898
+ const entity = entities ?? {};
899
+ const results = await fns.bulkEvaluate(id, flags, entity);
900
+ if (experimental_reportExposures && options?.experimental_exposureLogging !== false) {
901
+ const exposures = [];
902
+ const seen = /* @__PURE__ */ new Set();
903
+ for (const flag of flags) {
904
+ if (seen.has(flag.key)) continue;
905
+ seen.add(flag.key);
906
+ const result = results[flag.key];
907
+ if (!result) continue;
908
+ const exposure = getExposure(flag.key, result);
909
+ if (exposure) exposures.push(exposure);
910
+ }
911
+ report(exposures, entity);
912
+ }
913
+ return results;
914
+ },
915
+ experimental_reportOverride: async ({
916
+ key,
917
+ value,
918
+ entities
919
+ }) => {
920
+ if (!experimental_reportExposures) return;
921
+ try {
922
+ const instance = controllerInstanceMap.get(id);
923
+ if (!instance?.initialized) await api.initialize();
924
+ const datafile = await fns.getDatafile(id);
925
+ const definition = datafile.definitions[key];
926
+ const experiment = definition?.experiment;
927
+ if (!experiment) return;
928
+ const variantIndex = definition.variants.findIndex(
929
+ (variant) => dequal(variant, value)
930
+ );
931
+ const variantId = variantIndex < 0 ? null : definition.variantIds?.[variantIndex] ?? null;
932
+ const entity = entities ?? {};
933
+ report(
934
+ [
935
+ {
936
+ flagKey: key,
937
+ experimentId: experiment.id,
938
+ variantId,
939
+ base: experiment.base,
940
+ rampId: experiment.rampId,
941
+ rampPercentage: experiment.rampPercentage,
942
+ assignmentReason: "override"
943
+ }
944
+ ],
945
+ entity
946
+ );
947
+ } catch (error) {
948
+ console.error(
949
+ "@vercel/flags-core: Failed to report experiment override",
950
+ error
951
+ );
952
+ }
789
953
  }
790
954
  };
791
955
  return api;
@@ -964,7 +1128,7 @@ function getRequestContext() {
964
1128
  }
965
1129
 
966
1130
  // src/utils/scheduler.ts
967
- import { waitUntil } from "@vercel/functions";
1131
+ import { waitUntil as waitUntil2 } from "@vercel/functions";
968
1132
  var IDLE_FLUSH_WAIT_MS = 5e3;
969
1133
  var IDLE_FLUSH_JITTER_RATIO = 0.2;
970
1134
  var MAX_FLUSH_WAIT_MS = 6e4;
@@ -987,7 +1151,7 @@ var Scheduler = class {
987
1151
  await this.onFlush(reason);
988
1152
  })();
989
1153
  try {
990
- waitUntil(this.pending);
1154
+ waitUntil2(this.pending);
991
1155
  } catch {
992
1156
  }
993
1157
  this.maxTimeout = setTimeout(
@@ -1442,6 +1606,11 @@ var PollingSource = class extends TypedEmitter {
1442
1606
  }
1443
1607
  };
1444
1608
 
1609
+ // src/utils/runtime.ts
1610
+ function isBun() {
1611
+ return typeof globalThis.Bun !== "undefined";
1612
+ }
1613
+
1445
1614
  // src/utils/sleep.ts
1446
1615
  function sleep(ms) {
1447
1616
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -1526,6 +1695,9 @@ async function connectStream(config, callbacks) {
1526
1695
  "User-Agent": `VercelFlagsCore/${version}`,
1527
1696
  "X-Retry-Attempt": String(retryCount)
1528
1697
  };
1698
+ if (isBun()) {
1699
+ headers["Accept-Encoding"] = "identity";
1700
+ }
1529
1701
  const vercelEnv = process.env.VERCEL_ENV;
1530
1702
  if (vercelEnv) {
1531
1703
  headers["X-Vercel-Env"] = vercelEnv;
@@ -2430,11 +2602,13 @@ function make(createRawClient) {
2430
2602
  const optionsOnly = typeof sdkKeyOrConnectionStringOrOptions === "object" && sdkKeyOrConnectionStringOrOptions !== null;
2431
2603
  const sdkKeyOrConnectionString = optionsOnly ? void 0 : sdkKeyOrConnectionStringOrOptions;
2432
2604
  const createClientOptions = optionsOnly ? sdkKeyOrConnectionStringOrOptions : options;
2605
+ const { experimental_reportExposures, ...controllerOptions } = createClientOptions ?? {};
2433
2606
  const auth = new Authentication(sdkKeyOrConnectionString);
2434
- const controller = new Controller({ auth, ...createClientOptions });
2607
+ const controller = new Controller({ auth, ...controllerOptions });
2435
2608
  return createRawClient({
2436
2609
  controller,
2437
- origin: { provider: "vercel", sdkKey: auth.sdkKey }
2610
+ origin: { provider: "vercel", sdkKey: auth.sdkKey },
2611
+ ...experimental_reportExposures ? { experimental_reportExposures } : {}
2438
2612
  });
2439
2613
  }
2440
2614
  function resetDefaultFlagsClient2() {
@@ -2509,4 +2683,4 @@ export {
2509
2683
  resetDefaultFlagsClient,
2510
2684
  createClient
2511
2685
  };
2512
- //# sourceMappingURL=chunk-ZV3PRKK2.js.map
2686
+ //# sourceMappingURL=chunk-2QUTYVSI.js.map