@vercel/flags-core 1.8.0 → 1.8.1-873d051-20260908100141
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 +24 -0
- package/README.md +2 -0
- package/dist/{chunk-ZROGU3V7.js → chunk-2KQT4NSN.js} +208 -35
- package/dist/chunk-2KQT4NSN.js.map +1 -0
- package/dist/{chunk-ZV3PRKK2.js → chunk-5YWHI2ED.js} +208 -35
- package/dist/chunk-5YWHI2ED.js.map +1 -0
- package/dist/{chunk-ID4ZKPA3.cjs → chunk-K3MJC74D.cjs} +244 -71
- package/dist/chunk-K3MJC74D.cjs.map +1 -0
- package/dist/{chunk-C3FUDFOP.cjs → chunk-QKTGXYBH.cjs} +243 -70
- package/dist/chunk-QKTGXYBH.cjs.map +1 -0
- package/dist/index.default.cjs +2 -2
- package/dist/index.default.d.cts +14 -6
- package/dist/index.default.d.ts +14 -6
- package/dist/index.default.js +1 -1
- package/dist/index.next-js.cjs +2 -2
- package/dist/index.next-js.d.cts +14 -6
- package/dist/index.next-js.d.ts +14 -6
- package/dist/index.next-js.js +1 -1
- package/dist/openfeature.default.cjs +2 -2
- package/dist/openfeature.default.d.cts +1 -1
- package/dist/openfeature.default.d.ts +1 -1
- package/dist/openfeature.default.js +1 -1
- package/dist/openfeature.next-js.cjs +2 -2
- package/dist/openfeature.next-js.d.cts +1 -1
- package/dist/openfeature.next-js.d.ts +1 -1
- package/dist/openfeature.next-js.js +1 -1
- package/dist/{types-C2-sIv2D.d.cts → types-CIONStRG.d.cts} +87 -5
- package/dist/{types-C2-sIv2D.d.ts → types-CIONStRG.d.ts} +87 -5
- package/package.json +2 -1
- package/dist/chunk-C3FUDFOP.cjs.map +0 -1
- package/dist/chunk-ID4ZKPA3.cjs.map +0 -1
- package/dist/chunk-ZROGU3V7.js.map +0 -1
- package/dist/chunk-ZV3PRKK2.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @vercel/flags-core
|
|
2
2
|
|
|
3
|
+
## 1.8.1-873d051-20260908100141
|
|
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 `Accept-Encoding: gzip` on the `/v1/stream` connection.
|
|
24
|
+
|
|
25
|
+
The stream is long-lived NDJSON and the server flushes the compressor after every message. Runtimes such as Bun advertise `br` by default but do not surface partially decoded brotli output until the response ends, so the initial datafile never arrived and every flag fell back to its default after the init timeout. gzip streams correctly on Node, Bun, and browsers.
|
|
26
|
+
|
|
3
27
|
## 1.8.0
|
|
4
28
|
|
|
5
29
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -327,7 +327,43 @@ function getVariant(definition, index) {
|
|
|
327
327
|
variantId
|
|
328
328
|
};
|
|
329
329
|
}
|
|
330
|
-
function
|
|
330
|
+
function getWeightedVariantIndex(params, assignment, seed) {
|
|
331
|
+
const lhs = access(assignment.base, params);
|
|
332
|
+
if (typeof lhs !== "string") return assignment.defaultVariant;
|
|
333
|
+
const bucket = hashInput(lhs, seed);
|
|
334
|
+
const boundaries = getSplitBoundaries(assignment);
|
|
335
|
+
for (let index = 0; index < boundaries.length; index++) {
|
|
336
|
+
if (bucket < boundaries[index]) return index;
|
|
337
|
+
}
|
|
338
|
+
return assignment.defaultVariant;
|
|
339
|
+
}
|
|
340
|
+
function experimentAssignment(experiment, variantId, assignmentReason) {
|
|
341
|
+
if (variantId === null) return void 0;
|
|
342
|
+
return {
|
|
343
|
+
id: experiment.id,
|
|
344
|
+
variantId,
|
|
345
|
+
base: experiment.base,
|
|
346
|
+
rampId: experiment.rampId,
|
|
347
|
+
rampPercentage: experiment.rampPercentage,
|
|
348
|
+
assignmentReason
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function outcomeAssignmentReason(outcome) {
|
|
352
|
+
if (typeof outcome === "number") return "variant";
|
|
353
|
+
switch (outcome.type) {
|
|
354
|
+
case "experiment":
|
|
355
|
+
return "experiment";
|
|
356
|
+
case "split":
|
|
357
|
+
return "split";
|
|
358
|
+
case "rollout":
|
|
359
|
+
return "rollout";
|
|
360
|
+
default: {
|
|
361
|
+
const { type } = outcome;
|
|
362
|
+
return exhaustivenessCheck(type);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function resolveOutcome(params, outcome) {
|
|
331
367
|
if (typeof outcome === "number") {
|
|
332
368
|
const variant = getVariant(params.definition, outcome);
|
|
333
369
|
return {
|
|
@@ -337,32 +373,48 @@ function handleOutcome(params, outcome) {
|
|
|
337
373
|
}
|
|
338
374
|
switch (outcome.type) {
|
|
339
375
|
case "split": {
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
376
|
+
const index = getWeightedVariantIndex(
|
|
377
|
+
params,
|
|
378
|
+
outcome,
|
|
379
|
+
params.definition.seed
|
|
344
380
|
);
|
|
345
|
-
if (typeof lhs !== "string") {
|
|
346
|
-
return {
|
|
347
|
-
...defaultOutcome,
|
|
348
|
-
outcomeType: "split" /* SPLIT */
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
const bucket = hashInput(lhs, params.definition.seed);
|
|
352
|
-
const boundaries = getSplitBoundaries(outcome);
|
|
353
|
-
for (let index = 0; index < boundaries.length; index++) {
|
|
354
|
-
if (bucket < boundaries[index]) {
|
|
355
|
-
return {
|
|
356
|
-
...getVariant(params.definition, index),
|
|
357
|
-
outcomeType: "split" /* SPLIT */
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
381
|
return {
|
|
362
|
-
...
|
|
382
|
+
...getVariant(params.definition, index),
|
|
363
383
|
outcomeType: "split" /* SPLIT */
|
|
364
384
|
};
|
|
365
385
|
}
|
|
386
|
+
case "experiment": {
|
|
387
|
+
const experiment = params.definition.experiment;
|
|
388
|
+
if (!experiment) {
|
|
389
|
+
throw new Error("@vercel/flags-core: Experiment not found");
|
|
390
|
+
}
|
|
391
|
+
const unitValue = access(experiment.base, params);
|
|
392
|
+
const defaultVariant = getVariant(
|
|
393
|
+
params.definition,
|
|
394
|
+
experiment.defaultVariant
|
|
395
|
+
);
|
|
396
|
+
const assignment = (variant, assignmentReason) => ({
|
|
397
|
+
...variant,
|
|
398
|
+
outcomeType: "experiment" /* EXPERIMENT */,
|
|
399
|
+
experiment: experimentAssignment(
|
|
400
|
+
experiment,
|
|
401
|
+
variant.variantId,
|
|
402
|
+
assignmentReason
|
|
403
|
+
)
|
|
404
|
+
});
|
|
405
|
+
if (typeof unitValue !== "string") {
|
|
406
|
+
return assignment(defaultVariant, "not-enrolled");
|
|
407
|
+
}
|
|
408
|
+
const rampPercentage = experiment.rampPercentage ?? 100;
|
|
409
|
+
const enrolled = rampPercentage >= 100 || rampPercentage > 0 && hashInput(unitValue, experiment.enrollmentSeed) < boundaryFor(rampPercentage, 100);
|
|
410
|
+
if (!enrolled) return assignment(defaultVariant, "not-enrolled");
|
|
411
|
+
const index = getWeightedVariantIndex(
|
|
412
|
+
params,
|
|
413
|
+
experiment,
|
|
414
|
+
params.definition.seed
|
|
415
|
+
);
|
|
416
|
+
return assignment(getVariant(params.definition, index), "experiment");
|
|
417
|
+
}
|
|
366
418
|
case "rollout": {
|
|
367
419
|
const lhs = access(outcome.base, params);
|
|
368
420
|
const defaultOutcome = getVariant(
|
|
@@ -435,6 +487,19 @@ function handleOutcome(params, outcome) {
|
|
|
435
487
|
}
|
|
436
488
|
}
|
|
437
489
|
}
|
|
490
|
+
function handleOutcome(params, outcome, assignmentReason) {
|
|
491
|
+
const result = resolveOutcome(params, outcome);
|
|
492
|
+
const experiment = params.definition.experiment;
|
|
493
|
+
if (!experiment || result.experiment) return result;
|
|
494
|
+
return {
|
|
495
|
+
...result,
|
|
496
|
+
experiment: experimentAssignment(
|
|
497
|
+
experiment,
|
|
498
|
+
result.variantId,
|
|
499
|
+
assignmentReason ?? outcomeAssignmentReason(outcome)
|
|
500
|
+
)
|
|
501
|
+
};
|
|
502
|
+
}
|
|
438
503
|
function evaluate(params, _visited) {
|
|
439
504
|
const envConfig = params.definition.environments[params.environment];
|
|
440
505
|
if (typeof envConfig === "number") {
|
|
@@ -474,7 +539,7 @@ function evaluate(params, _visited) {
|
|
|
474
539
|
(targetList) => matchTargetList(targetList, params)
|
|
475
540
|
);
|
|
476
541
|
if (matchedIndex > -1) {
|
|
477
|
-
return Object.assign(handleOutcome(params, matchedIndex), {
|
|
542
|
+
return Object.assign(handleOutcome(params, matchedIndex, "targeted"), {
|
|
478
543
|
reason: "target_match" /* TARGET_MATCH */
|
|
479
544
|
});
|
|
480
545
|
}
|
|
@@ -508,7 +573,7 @@ function bulkEvaluate(flags, shared) {
|
|
|
508
573
|
}
|
|
509
574
|
|
|
510
575
|
// package.json
|
|
511
|
-
var version = "1.8.
|
|
576
|
+
var version = "1.8.1-873d051-20260908100141";
|
|
512
577
|
|
|
513
578
|
// src/lib/report-value.ts
|
|
514
579
|
function internalReportValue(key, value, data) {
|
|
@@ -727,6 +792,8 @@ async function bulkEvaluate2(id, flags, entities) {
|
|
|
727
792
|
}
|
|
728
793
|
|
|
729
794
|
// src/create-raw-client.ts
|
|
795
|
+
import { waitUntil } from "@vercel/functions";
|
|
796
|
+
import { dequal } from "dequal/lite";
|
|
730
797
|
var idCount = 0;
|
|
731
798
|
async function performInitialize(instance, initFn) {
|
|
732
799
|
try {
|
|
@@ -740,7 +807,8 @@ async function performInitialize(instance, initFn) {
|
|
|
740
807
|
function createCreateRawClient(fns) {
|
|
741
808
|
return function createRawClient({
|
|
742
809
|
controller,
|
|
743
|
-
origin
|
|
810
|
+
origin,
|
|
811
|
+
experimental_reportExposures
|
|
744
812
|
}) {
|
|
745
813
|
const id = idCount++;
|
|
746
814
|
controllerInstanceMap.set(id, {
|
|
@@ -748,6 +816,35 @@ function createCreateRawClient(fns) {
|
|
|
748
816
|
initialized: false,
|
|
749
817
|
initPromise: null
|
|
750
818
|
});
|
|
819
|
+
function report(exposures, entity) {
|
|
820
|
+
if (!experimental_reportExposures || exposures.length === 0) return;
|
|
821
|
+
const pending = (async () => {
|
|
822
|
+
try {
|
|
823
|
+
await experimental_reportExposures(exposures, entity);
|
|
824
|
+
} catch (error) {
|
|
825
|
+
console.error(
|
|
826
|
+
"@vercel/flags-core: Failed to report experiment exposures",
|
|
827
|
+
error
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
})();
|
|
831
|
+
try {
|
|
832
|
+
waitUntil(pending);
|
|
833
|
+
} catch {
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
function getExposure(flagKey, result) {
|
|
837
|
+
if (!result.experiment) return null;
|
|
838
|
+
return {
|
|
839
|
+
flagKey,
|
|
840
|
+
experimentId: result.experiment.id,
|
|
841
|
+
variantId: result.experiment.variantId,
|
|
842
|
+
base: result.experiment.base,
|
|
843
|
+
...result.experiment.rampId === void 0 ? {} : { rampId: result.experiment.rampId },
|
|
844
|
+
...result.experiment.rampPercentage === void 0 ? {} : { rampPercentage: result.experiment.rampPercentage },
|
|
845
|
+
assignmentReason: result.experiment.assignmentReason
|
|
846
|
+
};
|
|
847
|
+
}
|
|
751
848
|
const api = {
|
|
752
849
|
origin,
|
|
753
850
|
initialize: async () => {
|
|
@@ -782,7 +879,7 @@ function createCreateRawClient(fns) {
|
|
|
782
879
|
getFallbackDatafile: () => {
|
|
783
880
|
return fns.getFallbackDatafile(id);
|
|
784
881
|
},
|
|
785
|
-
evaluate: async (flagKey, defaultValue, entities) => {
|
|
882
|
+
evaluate: async (flagKey, defaultValue, entities, options) => {
|
|
786
883
|
const instance = controllerInstanceMap.get(id);
|
|
787
884
|
if (!instance?.initialized) {
|
|
788
885
|
try {
|
|
@@ -790,9 +887,22 @@ function createCreateRawClient(fns) {
|
|
|
790
887
|
} catch {
|
|
791
888
|
}
|
|
792
889
|
}
|
|
793
|
-
|
|
890
|
+
const entity = entities ?? {};
|
|
891
|
+
const result = await fns.evaluate(
|
|
892
|
+
id,
|
|
893
|
+
flagKey,
|
|
894
|
+
defaultValue,
|
|
895
|
+
entity
|
|
896
|
+
);
|
|
897
|
+
if (experimental_reportExposures && options?.experimental_exposureLogging !== false) {
|
|
898
|
+
const exposure = getExposure(flagKey, result);
|
|
899
|
+
if (exposure) {
|
|
900
|
+
report([exposure], entity);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return result;
|
|
794
904
|
},
|
|
795
|
-
bulkEvaluate: async (flags, entities) => {
|
|
905
|
+
bulkEvaluate: async (flags, entities, options) => {
|
|
796
906
|
const instance = controllerInstanceMap.get(id);
|
|
797
907
|
if (!instance?.initialized) {
|
|
798
908
|
try {
|
|
@@ -800,7 +910,61 @@ function createCreateRawClient(fns) {
|
|
|
800
910
|
} catch {
|
|
801
911
|
}
|
|
802
912
|
}
|
|
803
|
-
|
|
913
|
+
const entity = entities ?? {};
|
|
914
|
+
const results = await fns.bulkEvaluate(id, flags, entity);
|
|
915
|
+
if (experimental_reportExposures && options?.experimental_exposureLogging !== false) {
|
|
916
|
+
const exposures = [];
|
|
917
|
+
const seen = /* @__PURE__ */ new Set();
|
|
918
|
+
for (const flag of flags) {
|
|
919
|
+
if (seen.has(flag.key)) continue;
|
|
920
|
+
seen.add(flag.key);
|
|
921
|
+
const result = results[flag.key];
|
|
922
|
+
if (!result) continue;
|
|
923
|
+
const exposure = getExposure(flag.key, result);
|
|
924
|
+
if (exposure) exposures.push(exposure);
|
|
925
|
+
}
|
|
926
|
+
report(exposures, entity);
|
|
927
|
+
}
|
|
928
|
+
return results;
|
|
929
|
+
},
|
|
930
|
+
experimental_reportOverride: async ({
|
|
931
|
+
key,
|
|
932
|
+
value,
|
|
933
|
+
entities
|
|
934
|
+
}) => {
|
|
935
|
+
if (!experimental_reportExposures) return;
|
|
936
|
+
try {
|
|
937
|
+
const instance = controllerInstanceMap.get(id);
|
|
938
|
+
if (!instance?.initialized) await api.initialize();
|
|
939
|
+
const datafile = await fns.getDatafile(id);
|
|
940
|
+
const definition = datafile.definitions[key];
|
|
941
|
+
const experiment = definition?.experiment;
|
|
942
|
+
if (!experiment) return;
|
|
943
|
+
const variantIndex = definition.variants.findIndex(
|
|
944
|
+
(variant) => dequal(variant, value)
|
|
945
|
+
);
|
|
946
|
+
const variantId = variantIndex < 0 ? null : definition.variantIds?.[variantIndex] ?? null;
|
|
947
|
+
const entity = entities ?? {};
|
|
948
|
+
report(
|
|
949
|
+
[
|
|
950
|
+
{
|
|
951
|
+
flagKey: key,
|
|
952
|
+
experimentId: experiment.id,
|
|
953
|
+
variantId,
|
|
954
|
+
base: experiment.base,
|
|
955
|
+
rampId: experiment.rampId,
|
|
956
|
+
rampPercentage: experiment.rampPercentage,
|
|
957
|
+
assignmentReason: "override"
|
|
958
|
+
}
|
|
959
|
+
],
|
|
960
|
+
entity
|
|
961
|
+
);
|
|
962
|
+
} catch (error) {
|
|
963
|
+
console.error(
|
|
964
|
+
"@vercel/flags-core: Failed to report experiment override",
|
|
965
|
+
error
|
|
966
|
+
);
|
|
967
|
+
}
|
|
804
968
|
}
|
|
805
969
|
};
|
|
806
970
|
return api;
|
|
@@ -979,7 +1143,7 @@ function getRequestContext() {
|
|
|
979
1143
|
}
|
|
980
1144
|
|
|
981
1145
|
// src/utils/scheduler.ts
|
|
982
|
-
import { waitUntil } from "@vercel/functions";
|
|
1146
|
+
import { waitUntil as waitUntil2 } from "@vercel/functions";
|
|
983
1147
|
var IDLE_FLUSH_WAIT_MS = 5e3;
|
|
984
1148
|
var IDLE_FLUSH_JITTER_RATIO = 0.2;
|
|
985
1149
|
var MAX_FLUSH_WAIT_MS = 6e4;
|
|
@@ -1002,7 +1166,7 @@ var Scheduler = class {
|
|
|
1002
1166
|
await this.onFlush(reason);
|
|
1003
1167
|
})();
|
|
1004
1168
|
try {
|
|
1005
|
-
|
|
1169
|
+
waitUntil2(this.pending);
|
|
1006
1170
|
} catch {
|
|
1007
1171
|
}
|
|
1008
1172
|
this.maxTimeout = setTimeout(
|
|
@@ -1539,7 +1703,14 @@ async function connectStream(config, callbacks) {
|
|
|
1539
1703
|
const headers = {
|
|
1540
1704
|
Authorization: `Bearer ${token}`,
|
|
1541
1705
|
"User-Agent": `VercelFlagsCore/${version}`,
|
|
1542
|
-
"X-Retry-Attempt": String(retryCount)
|
|
1706
|
+
"X-Retry-Attempt": String(retryCount),
|
|
1707
|
+
// The stream is long-lived NDJSON; the server flushes the compressor
|
|
1708
|
+
// after every message. Some runtimes (Bun) advertise `br` by default
|
|
1709
|
+
// but do not surface partially decoded brotli output until the
|
|
1710
|
+
// response ends, so the first datafile never arrives and init times
|
|
1711
|
+
// out. gzip streams correctly everywhere, so request it explicitly.
|
|
1712
|
+
// See https://github.com/oven-sh/bun/issues/41439
|
|
1713
|
+
"Accept-Encoding": "gzip"
|
|
1543
1714
|
};
|
|
1544
1715
|
const vercelEnv = process.env.VERCEL_ENV;
|
|
1545
1716
|
if (vercelEnv) {
|
|
@@ -2445,11 +2616,13 @@ function make(createRawClient) {
|
|
|
2445
2616
|
const optionsOnly = typeof sdkKeyOrConnectionStringOrOptions === "object" && sdkKeyOrConnectionStringOrOptions !== null;
|
|
2446
2617
|
const sdkKeyOrConnectionString = optionsOnly ? void 0 : sdkKeyOrConnectionStringOrOptions;
|
|
2447
2618
|
const createClientOptions = optionsOnly ? sdkKeyOrConnectionStringOrOptions : options;
|
|
2619
|
+
const { experimental_reportExposures, ...controllerOptions } = createClientOptions ?? {};
|
|
2448
2620
|
const auth = new Authentication(sdkKeyOrConnectionString);
|
|
2449
|
-
const controller = new Controller({ auth, ...
|
|
2621
|
+
const controller = new Controller({ auth, ...controllerOptions });
|
|
2450
2622
|
return createRawClient({
|
|
2451
2623
|
controller,
|
|
2452
|
-
origin: { provider: "vercel", sdkKey: auth.sdkKey }
|
|
2624
|
+
origin: { provider: "vercel", sdkKey: auth.sdkKey },
|
|
2625
|
+
...experimental_reportExposures ? { experimental_reportExposures } : {}
|
|
2453
2626
|
});
|
|
2454
2627
|
}
|
|
2455
2628
|
function resetDefaultFlagsClient2() {
|
|
@@ -2499,4 +2672,4 @@ export {
|
|
|
2499
2672
|
resetDefaultFlagsClient,
|
|
2500
2673
|
createClient
|
|
2501
2674
|
};
|
|
2502
|
-
//# sourceMappingURL=chunk-
|
|
2675
|
+
//# sourceMappingURL=chunk-2KQT4NSN.js.map
|