@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/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
+ }
@@ -0,0 +1,5 @@
1
+ describe("sdk: Index", function () {
2
+ it("should be a function", function () {
3
+ expect(true).toBe(true);
4
+ });
5
+ });
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./bucket";
2
+ export * from "./client";
@@ -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
+ });
@@ -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,7 @@
1
+ {
2
+ "extends": "../../tsconfig.cjs.json",
3
+ "compilerOptions": {
4
+ "outDir": "./lib"
5
+ },
6
+ "include": ["./src/**/*.ts"]
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../tsconfig.esm.json",
3
+ "compilerOptions": {
4
+ "outDir": "./lib"
5
+ },
6
+ "include": ["./src/**/*.ts"]
7
+ }
@@ -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;