@featurevisor/sdk 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 +53 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/lib/bucket.d.ts +2 -0
- package/lib/bucket.js +10 -0
- package/lib/bucket.js.map +1 -0
- package/lib/bucket.spec.d.ts +1 -0
- package/lib/client.d.ts +49 -0
- package/lib/client.js +205 -0
- package/lib/client.js.map +1 -0
- package/lib/conditions.d.ts +3 -0
- package/lib/conditions.js +69 -0
- package/lib/conditions.js.map +1 -0
- package/lib/conditions.spec.d.ts +1 -0
- package/lib/datafileReader.d.ts +16 -0
- package/lib/datafileReader.js +49 -0
- package/lib/datafileReader.js.map +1 -0
- package/lib/feature.d.ts +8 -0
- package/lib/feature.js +117 -0
- package/lib/feature.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -0
- package/lib/index.spec.d.ts +0 -0
- package/lib/segments.d.ts +4 -0
- package/lib/segments.js +35 -0
- package/lib/segments.js.map +1 -0
- package/lib/segments.spec.d.ts +1 -0
- package/package.json +49 -0
- package/src/bucket.spec.ts +18 -0
- package/src/bucket.ts +13 -0
- package/src/client.ts +338 -0
- package/src/conditions.spec.ts +533 -0
- package/src/conditions.ts +73 -0
- package/src/datafileReader.ts +73 -0
- package/src/feature.ts +213 -0
- package/src/index.spec.ts +5 -0
- package/src/index.ts +2 -0
- package/src/segments.spec.ts +196 -0
- package/src/segments.ts +49 -0
- package/tsconfig.cjs.json +7 -0
- package/tsconfig.esm.json +7 -0
- package/webpack.config.js +13 -0
package/src/feature.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Allocation,
|
|
3
|
+
Attributes,
|
|
4
|
+
Traffic,
|
|
5
|
+
Feature,
|
|
6
|
+
Variation,
|
|
7
|
+
VariableKey,
|
|
8
|
+
VariableValue,
|
|
9
|
+
Force,
|
|
10
|
+
} from "@featurevisor/types";
|
|
11
|
+
import { DatafileReader } from "./datafileReader";
|
|
12
|
+
import { allGroupSegmentsAreMatched } from "./segments";
|
|
13
|
+
import { allConditionsAreMatched } from "./conditions";
|
|
14
|
+
|
|
15
|
+
export function getMatchedTraffic(
|
|
16
|
+
traffic: Traffic[],
|
|
17
|
+
attributes: Attributes,
|
|
18
|
+
bucketValue: number,
|
|
19
|
+
datafileReader: DatafileReader,
|
|
20
|
+
): Traffic | undefined {
|
|
21
|
+
return traffic.find((traffic) => {
|
|
22
|
+
if (bucketValue > traffic.percentage) {
|
|
23
|
+
// out of bucket range
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (
|
|
28
|
+
!allGroupSegmentsAreMatched(
|
|
29
|
+
typeof traffic.segments === "string" && traffic.segments !== "*"
|
|
30
|
+
? JSON.parse(traffic.segments)
|
|
31
|
+
: traffic.segments,
|
|
32
|
+
attributes,
|
|
33
|
+
datafileReader,
|
|
34
|
+
)
|
|
35
|
+
) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return true;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// @TODO: make this function better with tests
|
|
44
|
+
export function getMatchedAllocation(
|
|
45
|
+
matchedTraffic: Traffic,
|
|
46
|
+
bucketValue: number,
|
|
47
|
+
): Allocation | undefined {
|
|
48
|
+
let total = 0;
|
|
49
|
+
|
|
50
|
+
for (const allocation of matchedTraffic.allocation) {
|
|
51
|
+
total += allocation.percentage;
|
|
52
|
+
|
|
53
|
+
if (bucketValue <= total) {
|
|
54
|
+
return allocation;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function findForceFromFeature(
|
|
62
|
+
feature: Feature,
|
|
63
|
+
attributes: Attributes,
|
|
64
|
+
datafileReader: DatafileReader,
|
|
65
|
+
): Force | undefined {
|
|
66
|
+
if (!feature.force) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return feature.force.find((f: Force) => {
|
|
71
|
+
if (f.conditions) {
|
|
72
|
+
return allConditionsAreMatched(f.conditions, attributes);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (f.segments) {
|
|
76
|
+
return allGroupSegmentsAreMatched(f.segments, attributes, datafileReader);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return false;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getForcedVariation(
|
|
84
|
+
feature: Feature,
|
|
85
|
+
attributes: Attributes,
|
|
86
|
+
datafileReader: DatafileReader,
|
|
87
|
+
): Variation | undefined {
|
|
88
|
+
const force = findForceFromFeature(feature, attributes, datafileReader);
|
|
89
|
+
|
|
90
|
+
if (!force || !force.variation) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return feature.variations.find((v) => v.value === force.variation);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getBucketedVariation(
|
|
98
|
+
feature: Feature,
|
|
99
|
+
attributes: Attributes,
|
|
100
|
+
bucketValue: number,
|
|
101
|
+
datafileReader: DatafileReader,
|
|
102
|
+
): Variation | undefined {
|
|
103
|
+
const matchedTraffic = getMatchedTraffic(
|
|
104
|
+
feature.traffic,
|
|
105
|
+
attributes,
|
|
106
|
+
bucketValue,
|
|
107
|
+
datafileReader,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (!matchedTraffic) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const allocation = getMatchedAllocation(matchedTraffic, bucketValue);
|
|
115
|
+
|
|
116
|
+
if (!allocation) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const variationValue = allocation.variation;
|
|
121
|
+
|
|
122
|
+
const variation = feature.variations.find((v) => {
|
|
123
|
+
return v.value === variationValue;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (!variation) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return variation;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function getForcedVariableValue(
|
|
134
|
+
feature: Feature,
|
|
135
|
+
variableKey: VariableKey,
|
|
136
|
+
attributes: Attributes,
|
|
137
|
+
datafileReader: DatafileReader,
|
|
138
|
+
): VariableValue | undefined {
|
|
139
|
+
const force = findForceFromFeature(feature, attributes, datafileReader);
|
|
140
|
+
|
|
141
|
+
if (!force || !force.variables) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return force.variables[variableKey];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function getBucketedVariableValue(
|
|
149
|
+
feature: Feature,
|
|
150
|
+
variableKey: VariableKey,
|
|
151
|
+
attributes: Attributes,
|
|
152
|
+
bucketValue: number,
|
|
153
|
+
datafileReader: DatafileReader,
|
|
154
|
+
): VariableValue | undefined {
|
|
155
|
+
// all variables
|
|
156
|
+
const variablesSchema = feature.variablesSchema;
|
|
157
|
+
|
|
158
|
+
if (!variablesSchema) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// single variable
|
|
163
|
+
const variableSchema = variablesSchema.find((v) => {
|
|
164
|
+
v.key === variableKey;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (variableSchema) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const variation = getBucketedVariation(feature, attributes, bucketValue, datafileReader);
|
|
172
|
+
|
|
173
|
+
if (!variation) {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const variableFromVariation = variation.variables?.find((v) => {
|
|
178
|
+
return v.key === variableKey;
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (!variableFromVariation) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (variableFromVariation.overrides) {
|
|
186
|
+
const override = variableFromVariation.overrides.find((o) => {
|
|
187
|
+
if (o.conditions) {
|
|
188
|
+
return allConditionsAreMatched(
|
|
189
|
+
typeof o.conditions === "string" ? JSON.parse(o.conditions) : o.conditions,
|
|
190
|
+
attributes,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (o.segments) {
|
|
195
|
+
return allGroupSegmentsAreMatched(
|
|
196
|
+
typeof o.segments === "string" && o.segments !== "*"
|
|
197
|
+
? JSON.parse(o.segments)
|
|
198
|
+
: o.segments,
|
|
199
|
+
attributes,
|
|
200
|
+
datafileReader,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return false;
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (override) {
|
|
208
|
+
return override.value;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return variableFromVariation.value;
|
|
213
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { DatafileReader } from "./datafileReader";
|
|
2
|
+
import { allGroupSegmentsAreMatched } from "./segments";
|
|
3
|
+
import { DatafileContent, GroupSegment } from "@featurevisor/types";
|
|
4
|
+
|
|
5
|
+
interface Group {
|
|
6
|
+
key: string;
|
|
7
|
+
segments: GroupSegment | GroupSegment[] | "*";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("sdk: Segments", function () {
|
|
11
|
+
it("should be a function", function () {
|
|
12
|
+
expect(typeof allGroupSegmentsAreMatched).toEqual("function");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("datafile #1", function () {
|
|
16
|
+
const groups = [
|
|
17
|
+
// everyone
|
|
18
|
+
{
|
|
19
|
+
key: "*",
|
|
20
|
+
segments: "*",
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
// dutch
|
|
24
|
+
{
|
|
25
|
+
key: "dutchMobileUsers",
|
|
26
|
+
segments: ["mobileUsers", "netherlands"],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: "dutchMobileUsers2",
|
|
30
|
+
segments: {
|
|
31
|
+
and: ["mobileUsers", "netherlands"],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: "dutchMobileOrDesktopUsers",
|
|
36
|
+
segments: ["netherlands", { or: ["mobileUsers", "desktopUsers"] }],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: "dutchMobileOrDesktopUsers2",
|
|
40
|
+
segments: {
|
|
41
|
+
and: ["netherlands", { or: ["mobileUsers", "desktopUsers"] }],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const datafileContent: DatafileContent = {
|
|
47
|
+
schemaVersion: "1.0",
|
|
48
|
+
revision: "1",
|
|
49
|
+
features: [],
|
|
50
|
+
attributes: [],
|
|
51
|
+
|
|
52
|
+
segments: [
|
|
53
|
+
// deviceType
|
|
54
|
+
{
|
|
55
|
+
key: "mobileUsers",
|
|
56
|
+
conditions: [
|
|
57
|
+
{
|
|
58
|
+
attribute: "deviceType",
|
|
59
|
+
operator: "equals",
|
|
60
|
+
value: "mobile",
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: "desktopUsers",
|
|
66
|
+
conditions: [
|
|
67
|
+
{
|
|
68
|
+
attribute: "deviceType",
|
|
69
|
+
operator: "equals",
|
|
70
|
+
value: "desktop",
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// browser
|
|
76
|
+
{
|
|
77
|
+
key: "chromeBrowser",
|
|
78
|
+
conditions: [
|
|
79
|
+
{
|
|
80
|
+
attribute: "browser",
|
|
81
|
+
operator: "equals",
|
|
82
|
+
value: "chrome",
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: "firefoxBrowser",
|
|
88
|
+
conditions: [
|
|
89
|
+
{
|
|
90
|
+
attribute: "browser",
|
|
91
|
+
operator: "equals",
|
|
92
|
+
value: "firefox",
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// country
|
|
98
|
+
{
|
|
99
|
+
key: "netherlands",
|
|
100
|
+
conditions: [
|
|
101
|
+
{
|
|
102
|
+
attribute: "country",
|
|
103
|
+
operator: "equals",
|
|
104
|
+
value: "nl",
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
key: "germany",
|
|
110
|
+
conditions: [
|
|
111
|
+
{
|
|
112
|
+
attribute: "country",
|
|
113
|
+
operator: "equals",
|
|
114
|
+
value: "de",
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const datafileReader = new DatafileReader(datafileContent);
|
|
122
|
+
|
|
123
|
+
it("should match everyone", function () {
|
|
124
|
+
const group = groups.find((g) => g.key === "*") as Group;
|
|
125
|
+
|
|
126
|
+
// match
|
|
127
|
+
expect(allGroupSegmentsAreMatched(group.segments, {}, datafileReader)).toEqual(true);
|
|
128
|
+
expect(allGroupSegmentsAreMatched(group.segments, { foo: "foo" }, datafileReader)).toEqual(
|
|
129
|
+
true,
|
|
130
|
+
);
|
|
131
|
+
expect(allGroupSegmentsAreMatched(group.segments, { bar: "bar" }, datafileReader)).toEqual(
|
|
132
|
+
true,
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should match dutchMobileUsers", function () {
|
|
137
|
+
const group = groups.find((g) => g.key === "dutchMobileUsers") as Group;
|
|
138
|
+
|
|
139
|
+
// match
|
|
140
|
+
expect(
|
|
141
|
+
allGroupSegmentsAreMatched(
|
|
142
|
+
group.segments,
|
|
143
|
+
{ country: "nl", deviceType: "mobile" },
|
|
144
|
+
datafileReader,
|
|
145
|
+
),
|
|
146
|
+
).toEqual(true);
|
|
147
|
+
expect(
|
|
148
|
+
allGroupSegmentsAreMatched(
|
|
149
|
+
group.segments,
|
|
150
|
+
{ country: "nl", deviceType: "mobile", browser: "chrome" },
|
|
151
|
+
datafileReader,
|
|
152
|
+
),
|
|
153
|
+
).toEqual(true);
|
|
154
|
+
|
|
155
|
+
// not match
|
|
156
|
+
expect(allGroupSegmentsAreMatched(group.segments, {}, datafileReader)).toEqual(false);
|
|
157
|
+
expect(
|
|
158
|
+
allGroupSegmentsAreMatched(
|
|
159
|
+
group.segments,
|
|
160
|
+
{ country: "de", deviceType: "mobile" },
|
|
161
|
+
datafileReader,
|
|
162
|
+
),
|
|
163
|
+
).toEqual(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should match dutchMobileUsers2", function () {
|
|
167
|
+
const group = groups.find((g) => g.key === "dutchMobileUsers") as Group;
|
|
168
|
+
|
|
169
|
+
// match
|
|
170
|
+
expect(
|
|
171
|
+
allGroupSegmentsAreMatched(
|
|
172
|
+
group.segments,
|
|
173
|
+
{ country: "nl", deviceType: "mobile" },
|
|
174
|
+
datafileReader,
|
|
175
|
+
),
|
|
176
|
+
).toEqual(true);
|
|
177
|
+
expect(
|
|
178
|
+
allGroupSegmentsAreMatched(
|
|
179
|
+
group.segments,
|
|
180
|
+
{ country: "nl", deviceType: "mobile", browser: "chrome" },
|
|
181
|
+
datafileReader,
|
|
182
|
+
),
|
|
183
|
+
).toEqual(true);
|
|
184
|
+
|
|
185
|
+
// not match
|
|
186
|
+
expect(allGroupSegmentsAreMatched(group.segments, {}, datafileReader)).toEqual(false);
|
|
187
|
+
expect(
|
|
188
|
+
allGroupSegmentsAreMatched(
|
|
189
|
+
group.segments,
|
|
190
|
+
{ country: "de", deviceType: "mobile" },
|
|
191
|
+
datafileReader,
|
|
192
|
+
),
|
|
193
|
+
).toEqual(false);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
package/src/segments.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Attributes, GroupSegment, Segment, Condition } from "@featurevisor/types";
|
|
2
|
+
import { allConditionsAreMatched } from "./conditions";
|
|
3
|
+
import { DatafileReader } from "./datafileReader";
|
|
4
|
+
|
|
5
|
+
export function segmentIsMatched(segment: Segment, attributes: Attributes): boolean {
|
|
6
|
+
return allConditionsAreMatched(segment.conditions as Condition | Condition[], attributes);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function allGroupSegmentsAreMatched(
|
|
10
|
+
groupSegments: GroupSegment | GroupSegment[] | "*",
|
|
11
|
+
attributes: Attributes,
|
|
12
|
+
datafileReader: DatafileReader,
|
|
13
|
+
): boolean {
|
|
14
|
+
if (groupSegments === "*") {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (typeof groupSegments === "string") {
|
|
19
|
+
const segment = datafileReader.getSegment(groupSegments);
|
|
20
|
+
|
|
21
|
+
if (segment) {
|
|
22
|
+
return segmentIsMatched(segment, attributes);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof groupSegments === "object") {
|
|
29
|
+
if ("and" in groupSegments && Array.isArray(groupSegments.and)) {
|
|
30
|
+
return groupSegments.and.every((groupSegment) =>
|
|
31
|
+
allGroupSegmentsAreMatched(groupSegment, attributes, datafileReader),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if ("or" in groupSegments && Array.isArray(groupSegments.or)) {
|
|
36
|
+
return groupSegments.or.some((groupSegment) =>
|
|
37
|
+
allGroupSegmentsAreMatched(groupSegment, attributes, datafileReader),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (Array.isArray(groupSegments)) {
|
|
43
|
+
return groupSegments.every((groupSegment) =>
|
|
44
|
+
allGroupSegmentsAreMatched(groupSegment, attributes, datafileReader),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
3
|
+
const getWebpackConfig = require("../../tools/getWebpackConfig");
|
|
4
|
+
|
|
5
|
+
const wepbackConfig = getWebpackConfig({
|
|
6
|
+
entryFilePath: path.join(__dirname, "src", "index.ts"),
|
|
7
|
+
entryKey: "index",
|
|
8
|
+
outputDirectoryPath: path.join(__dirname, "dist"),
|
|
9
|
+
outputLibrary: "FeaturevisorSDK",
|
|
10
|
+
tsConfigFilePath: path.join(__dirname, "tsconfig.cjs.json"),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
module.exports = wepbackConfig;
|