@featurevisor/core 0.0.3

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/src/traffic.ts ADDED
@@ -0,0 +1,136 @@
1
+ import { Rule, ExistingFeature, Traffic, Variation } from "@featurevisor/types";
2
+ import { MAX_BUCKETED_NUMBER } from "@featurevisor/sdk";
3
+
4
+ export function getNewTraffic(
5
+ // from current YAML
6
+ variations: Variation[],
7
+ parsedRules: Rule[],
8
+
9
+ // from previous release
10
+ existingFeature: ExistingFeature | undefined,
11
+ ): Traffic[] {
12
+ const result: Traffic[] = [];
13
+
14
+ parsedRules.forEach((parsedRollout) => {
15
+ const rolloutPercentage = parsedRollout.percentage;
16
+
17
+ const traffic: Traffic = {
18
+ key: parsedRollout.key, // @TODO: not needed in datafile. keep it for now
19
+ segments:
20
+ typeof parsedRollout.segments !== "string"
21
+ ? JSON.stringify(parsedRollout.segments)
22
+ : parsedRollout.segments,
23
+ percentage: rolloutPercentage * (MAX_BUCKETED_NUMBER / 100),
24
+ allocation: [],
25
+ };
26
+
27
+ const existingTrafficRollout = existingFeature?.traffic.find(
28
+ (t) => t.key === parsedRollout.key,
29
+ );
30
+
31
+ // @TODO: handle if Variations changed (added/removed, or weight changed)
32
+
33
+ // - new variation added
34
+ // - variation removed
35
+ // - variation weight changed
36
+ //
37
+ // make it better by maintaining as much of the previous bucketing as possible
38
+ const variationsChanged = existingFeature
39
+ ? JSON.stringify(
40
+ existingFeature.variations.map(({ value, weight }) => ({
41
+ value,
42
+ weight,
43
+ })),
44
+ ) !== JSON.stringify(variations.map(({ value, weight }) => ({ value, weight })))
45
+ : false;
46
+
47
+ let diffPercentage = 0;
48
+
49
+ if (existingTrafficRollout) {
50
+ diffPercentage =
51
+ rolloutPercentage - existingTrafficRollout.percentage / (MAX_BUCKETED_NUMBER / 100);
52
+
53
+ if (
54
+ diffPercentage > 0 &&
55
+ !variationsChanged // if variations changed, we need to re-bucket
56
+ ) {
57
+ // increase: build on top of existing allocations
58
+
59
+ traffic.allocation = existingTrafficRollout.allocation.map(({ variation, percentage }) => {
60
+ return {
61
+ variation,
62
+ percentage,
63
+ };
64
+ });
65
+ }
66
+ }
67
+
68
+ variations.forEach((variation) => {
69
+ const newPercentage = parseInt(
70
+ (
71
+ ((variation.weight as number) / 100) *
72
+ rolloutPercentage *
73
+ (MAX_BUCKETED_NUMBER / 100)
74
+ ).toFixed(2),
75
+ );
76
+
77
+ if (!existingTrafficRollout || variationsChanged === true) {
78
+ traffic.allocation.push({
79
+ variation: variation.value,
80
+ percentage: newPercentage,
81
+ });
82
+
83
+ return;
84
+ }
85
+
86
+ // const prevTotalWeightForVariation = existingTrafficRollout.allocation
87
+ // .filter((a) => a.variation === variation.value)
88
+ // .reduce((acc, curr) => acc + curr.percentage, 0);
89
+
90
+ // const diffWeightForVariation = (variation.weight as number) - prevTotalWeightForVariation / (MAX_BUCKETED_NUMBER / 100));
91
+
92
+ if (diffPercentage === 0) {
93
+ // no change
94
+ traffic.allocation.push({
95
+ variation: variation.value,
96
+ percentage: newPercentage,
97
+ });
98
+
99
+ return;
100
+ }
101
+
102
+ if (diffPercentage > 0) {
103
+ // increase - need to consistently bucket
104
+ traffic.allocation.push({
105
+ variation: variation.value,
106
+ percentage: parseInt(
107
+ (
108
+ (variation.weight as number) *
109
+ (diffPercentage / 100) *
110
+ (MAX_BUCKETED_NUMBER / 100)
111
+ ).toFixed(2),
112
+ ),
113
+ });
114
+
115
+ return;
116
+ }
117
+
118
+ if (diffPercentage < 0) {
119
+ // decrease - need to re-bucket
120
+
121
+ // @TODO: can we maintain as much pre bucketed values as possible? to be close to consistent bucketing?
122
+
123
+ traffic.allocation.push({
124
+ variation: variation.value,
125
+ percentage: newPercentage,
126
+ });
127
+
128
+ return;
129
+ }
130
+ });
131
+
132
+ result.push(traffic);
133
+ });
134
+
135
+ return result;
136
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,83 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+
4
+ import * as yaml from "js-yaml";
5
+
6
+ import { Condition, AttributeKey, GroupSegment, SegmentKey } from "@featurevisor/types";
7
+
8
+ export function getYAMLFiles(directoryPath: string) {
9
+ const files = fs.readdirSync(directoryPath);
10
+ const yamlFiles = files.filter((file) => file.endsWith(".yml"));
11
+
12
+ return yamlFiles.map((file) => path.join(directoryPath, file));
13
+ }
14
+
15
+ export function parseYaml(content: string) {
16
+ const parsed = yaml.load(content);
17
+
18
+ return parsed;
19
+ }
20
+
21
+ export function extractSegmentKeysFromGroupSegments(
22
+ segments: GroupSegment | GroupSegment[],
23
+ ): Set<SegmentKey> {
24
+ const result = new Set<SegmentKey>();
25
+
26
+ if (Array.isArray(segments)) {
27
+ segments.forEach((segment) => {
28
+ extractSegmentKeysFromGroupSegments(segment).forEach((segmentKey) => {
29
+ result.add(segmentKey);
30
+ });
31
+ });
32
+ }
33
+
34
+ if (typeof segments === "object") {
35
+ if ("and" in segments) {
36
+ extractSegmentKeysFromGroupSegments(segments.and).forEach((segmentKey) => {
37
+ result.add(segmentKey);
38
+ });
39
+ }
40
+
41
+ if ("or" in segments) {
42
+ extractSegmentKeysFromGroupSegments(segments.or).forEach((segmentKey) => {
43
+ result.add(segmentKey);
44
+ });
45
+ }
46
+ }
47
+
48
+ if (typeof segments === "string") {
49
+ result.add(segments);
50
+ }
51
+
52
+ return result;
53
+ }
54
+
55
+ export function extractAttributeKeysFromConditions(conditions: Condition | Condition[]) {
56
+ const result = new Set<AttributeKey>();
57
+
58
+ if (Array.isArray(conditions)) {
59
+ conditions.forEach((condition) => {
60
+ extractAttributeKeysFromConditions(condition).forEach((attributeKey) => {
61
+ result.add(attributeKey);
62
+ });
63
+ });
64
+ }
65
+
66
+ if ("attribute" in conditions) {
67
+ result.add(conditions.attribute);
68
+ }
69
+
70
+ if ("and" in conditions) {
71
+ extractAttributeKeysFromConditions(conditions.and).forEach((attributeKey) => {
72
+ result.add(attributeKey);
73
+ });
74
+ }
75
+
76
+ if ("or" in conditions) {
77
+ extractAttributeKeysFromConditions(conditions.or).forEach((attributeKey) => {
78
+ result.add(attributeKey);
79
+ });
80
+ }
81
+
82
+ return result;
83
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../tsconfig.cjs.json",
3
+ "compilerOptions": {
4
+ "outDir": "./lib"
5
+ },
6
+ "include": ["./src/**/*.ts"]
7
+ }