@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/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +15 -0
- package/lib/builder.d.ts +12 -0
- package/lib/builder.js +245 -0
- package/lib/builder.js.map +1 -0
- package/lib/config.d.ts +25 -0
- package/lib/config.js +41 -0
- package/lib/config.js.map +1 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +22 -0
- package/lib/index.js.map +1 -0
- package/lib/init.d.ts +6 -0
- package/lib/init.js +38 -0
- package/lib/init.js.map +1 -0
- package/lib/linter.d.ts +9 -0
- package/lib/linter.js +334 -0
- package/lib/linter.js.map +1 -0
- package/lib/tester.d.ts +26 -0
- package/lib/tester.js +98 -0
- package/lib/tester.js.map +1 -0
- package/lib/traffic.d.ts +2 -0
- package/lib/traffic.js +105 -0
- package/lib/traffic.js.map +1 -0
- package/lib/traffic.spec.d.ts +1 -0
- package/lib/traffic.spec.js +636 -0
- package/lib/traffic.spec.js.map +1 -0
- package/lib/utils.d.ts +5 -0
- package/lib/utils.js +70 -0
- package/lib/utils.js.map +1 -0
- package/package.json +57 -0
- package/src/builder.ts +359 -0
- package/src/config.ts +62 -0
- package/src/index.ts +5 -0
- package/src/init.ts +44 -0
- package/src/linter.ts +328 -0
- package/src/tester.ts +163 -0
- package/src/traffic.spec.ts +681 -0
- package/src/traffic.ts +136 -0
- package/src/utils.ts +83 -0
- package/tsconfig.cjs.json +7 -0
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
|
+
}
|