@featurevisor/sdk 1.35.3 → 2.0.1
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 +8 -0
- package/README.md +2 -381
- package/coverage/clover.xml +707 -645
- package/coverage/coverage-final.json +11 -9
- package/coverage/lcov-report/{segments.ts.html → bucketer.ts.html} +155 -77
- package/coverage/lcov-report/child.ts.html +940 -0
- package/coverage/lcov-report/conditions.ts.html +107 -158
- package/coverage/lcov-report/datafileReader.ts.html +763 -103
- package/coverage/lcov-report/emitter.ts.html +77 -59
- package/coverage/lcov-report/evaluate.ts.html +689 -416
- package/coverage/lcov-report/events.ts.html +334 -0
- package/coverage/lcov-report/helpers.ts.html +184 -0
- package/coverage/lcov-report/{bucket.ts.html → hooks.ts.html} +86 -239
- package/coverage/lcov-report/index.html +119 -89
- package/coverage/lcov-report/instance.ts.html +341 -773
- package/coverage/lcov-report/logger.ts.html +64 -64
- package/coverage/lcov.info +1433 -1226
- package/dist/bucketer.d.ts +11 -0
- package/dist/child.d.ts +26 -0
- package/dist/compareVersions.d.ts +4 -0
- package/dist/conditions.d.ts +4 -4
- package/dist/datafileReader.d.ts +26 -6
- package/dist/emitter.d.ts +8 -9
- package/dist/evaluate.d.ts +31 -29
- package/dist/events.d.ts +5 -0
- package/dist/helpers.d.ts +5 -0
- package/dist/hooks.d.ts +45 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.gz +0 -0
- package/dist/index.mjs.map +1 -1
- package/dist/instance.d.ts +40 -72
- package/dist/logger.d.ts +6 -5
- package/dist/murmurhash.d.ts +1 -0
- package/jest.config.js +2 -0
- package/lib/bucketer.d.ts +11 -0
- package/lib/child.d.ts +26 -0
- package/lib/compareVersions.d.ts +4 -0
- package/lib/conditions.d.ts +4 -4
- package/lib/datafileReader.d.ts +26 -6
- package/lib/emitter.d.ts +8 -9
- package/lib/evaluate.d.ts +31 -29
- package/lib/events.d.ts +5 -0
- package/lib/helpers.d.ts +5 -0
- package/lib/hooks.d.ts +45 -0
- package/lib/index.d.ts +3 -2
- package/lib/instance.d.ts +40 -72
- package/lib/logger.d.ts +6 -5
- package/lib/murmurhash.d.ts +1 -0
- package/package.json +3 -5
- package/src/bucketer.spec.ts +165 -0
- package/src/bucketer.ts +84 -0
- package/src/child.spec.ts +267 -0
- package/src/child.ts +285 -0
- package/src/compareVersions.ts +93 -0
- package/src/conditions.spec.ts +563 -353
- package/src/conditions.ts +46 -63
- package/src/datafileReader.spec.ts +396 -84
- package/src/datafileReader.ts +280 -60
- package/src/emitter.spec.ts +27 -86
- package/src/emitter.ts +38 -32
- package/src/evaluate.ts +349 -258
- package/src/events.spec.ts +154 -0
- package/src/events.ts +83 -0
- package/src/helpers.ts +33 -0
- package/src/hooks.ts +88 -0
- package/src/index.ts +3 -2
- package/src/instance.spec.ts +305 -489
- package/src/instance.ts +247 -391
- package/src/logger.spec.ts +212 -134
- package/src/logger.ts +36 -36
- package/src/murmurhash.ts +71 -0
- package/coverage/lcov-report/feature.ts.html +0 -508
- package/dist/bucket.d.ts +0 -30
- package/dist/feature.d.ts +0 -16
- package/dist/segments.d.ts +0 -5
- package/lib/bucket.d.ts +0 -30
- package/lib/feature.d.ts +0 -16
- package/lib/segments.d.ts +0 -5
- package/src/bucket.spec.ts +0 -37
- package/src/bucket.ts +0 -139
- package/src/feature.ts +0 -141
- package/src/segments.spec.ts +0 -468
- package/src/segments.ts +0 -58
package/src/evaluate.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Feature,
|
|
1
|
+
import type {
|
|
3
2
|
FeatureKey,
|
|
4
3
|
Context,
|
|
5
4
|
BucketKey,
|
|
@@ -8,52 +7,53 @@ import {
|
|
|
8
7
|
Traffic,
|
|
9
8
|
Force,
|
|
10
9
|
Required,
|
|
11
|
-
OverrideFeature,
|
|
12
10
|
Variation,
|
|
13
11
|
VariationValue,
|
|
14
12
|
VariableKey,
|
|
15
13
|
VariableValue,
|
|
16
14
|
VariableSchema,
|
|
15
|
+
EvaluatedFeature,
|
|
17
16
|
StickyFeatures,
|
|
18
|
-
InitialFeatures,
|
|
19
17
|
Allocation,
|
|
20
18
|
} from "@featurevisor/types";
|
|
21
19
|
|
|
22
20
|
import { Logger } from "./logger";
|
|
21
|
+
import { HooksManager } from "./hooks";
|
|
23
22
|
import { DatafileReader } from "./datafileReader";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
getMatchedTraffic,
|
|
27
|
-
getMatchedTrafficAndAllocation,
|
|
28
|
-
findForceFromFeature,
|
|
29
|
-
parseFromStringifiedSegments,
|
|
30
|
-
} from "./feature";
|
|
31
|
-
import { allConditionsAreMatched } from "./conditions";
|
|
32
|
-
import { allGroupSegmentsAreMatched } from "./segments";
|
|
33
|
-
import type { Statuses, InterceptContext } from "./instance";
|
|
23
|
+
import { getBucketKey, getBucketedNumber } from "./bucketer";
|
|
34
24
|
|
|
35
25
|
export enum EvaluationReason {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
26
|
+
// feature specific
|
|
27
|
+
FEATURE_NOT_FOUND = "feature_not_found", // feature is not found in datafile
|
|
28
|
+
DISABLED = "disabled", // feature is disabled
|
|
29
|
+
REQUIRED = "required", // required features are not enabled
|
|
30
|
+
OUT_OF_RANGE = "out_of_range", // out of range when mutually exclusive experiments are involved via Groups
|
|
31
|
+
|
|
32
|
+
// variations specific
|
|
33
|
+
NO_VARIATIONS = "no_variations", // feature has no variations
|
|
34
|
+
VARIATION_DISABLED = "variation_disabled", // feature is disabled, and variation's disabledVariationValue is used
|
|
35
|
+
|
|
36
|
+
// variable specific
|
|
37
|
+
VARIABLE_NOT_FOUND = "variable_not_found", // variable's schema is not defined in the feature
|
|
38
|
+
VARIABLE_DEFAULT = "variable_default", // default variable value used
|
|
39
|
+
VARIABLE_DISABLED = "variable_disabled", // feature is disabled, and variable's disabledValue is used
|
|
40
|
+
VARIABLE_OVERRIDE = "variable_override", // variable overridden from inside a variation
|
|
41
|
+
|
|
42
|
+
// common
|
|
43
|
+
NO_MATCH = "no_match", // no rules matched
|
|
44
|
+
FORCED = "forced", // against a forced rule
|
|
45
|
+
STICKY = "sticky", // against a sticky feature
|
|
46
|
+
RULE = "rule", // against a regular rule
|
|
47
|
+
ALLOCATED = "allocated", // regular allocation based on bucketing
|
|
48
|
+
|
|
49
|
+
ERROR = "error", // error
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
type EvaluationType = "flag" | "variation" | "variable";
|
|
53
53
|
|
|
54
54
|
export interface Evaluation {
|
|
55
55
|
// required
|
|
56
|
-
|
|
56
|
+
type: EvaluationType;
|
|
57
57
|
featureKey: FeatureKey;
|
|
58
58
|
reason: EvaluationReason;
|
|
59
59
|
|
|
@@ -67,8 +67,7 @@ export interface Evaluation {
|
|
|
67
67
|
forceIndex?: number;
|
|
68
68
|
force?: Force;
|
|
69
69
|
required?: Required[];
|
|
70
|
-
sticky?:
|
|
71
|
-
initial?: OverrideFeature;
|
|
70
|
+
sticky?: EvaluatedFeature;
|
|
72
71
|
|
|
73
72
|
// variation
|
|
74
73
|
variation?: Variation;
|
|
@@ -80,47 +79,95 @@ export interface Evaluation {
|
|
|
80
79
|
variableSchema?: VariableSchema;
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
export interface
|
|
84
|
-
type: EvaluationType;
|
|
85
|
-
|
|
86
|
-
featureKey: FeatureKey | Feature;
|
|
87
|
-
variableKey?: VariableKey;
|
|
82
|
+
export interface EvaluateDependencies {
|
|
88
83
|
context: Context;
|
|
89
84
|
|
|
90
85
|
logger: Logger;
|
|
86
|
+
hooksManager: HooksManager;
|
|
91
87
|
datafileReader: DatafileReader;
|
|
92
|
-
statuses?: Statuses;
|
|
93
|
-
interceptContext?: InterceptContext;
|
|
94
88
|
|
|
95
|
-
|
|
96
|
-
|
|
89
|
+
// OverrideOptions
|
|
90
|
+
sticky?: StickyFeatures;
|
|
91
|
+
|
|
92
|
+
defaultVariationValue?: VariationValue;
|
|
93
|
+
defaultVariableValue?: VariableValue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface EvaluateParams {
|
|
97
|
+
type: EvaluationType;
|
|
98
|
+
featureKey: FeatureKey;
|
|
99
|
+
variableKey?: VariableKey;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type EvaluateOptions = EvaluateParams & EvaluateDependencies;
|
|
103
|
+
|
|
104
|
+
export function evaluateWithHooks(opts: EvaluateOptions): Evaluation {
|
|
105
|
+
try {
|
|
106
|
+
const { hooksManager } = opts;
|
|
107
|
+
const hooks = hooksManager.getAll();
|
|
108
|
+
|
|
109
|
+
// run before hooks
|
|
110
|
+
let options = opts;
|
|
111
|
+
for (const hook of hooksManager.getAll()) {
|
|
112
|
+
if (hook.before) {
|
|
113
|
+
options = hook.before(options);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// evaluate
|
|
118
|
+
let evaluation = evaluate(options);
|
|
97
119
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
120
|
+
// default: variation
|
|
121
|
+
if (
|
|
122
|
+
typeof options.defaultVariationValue !== "undefined" &&
|
|
123
|
+
evaluation.type === "variation" &&
|
|
124
|
+
typeof evaluation.variationValue === "undefined"
|
|
125
|
+
) {
|
|
126
|
+
evaluation.variationValue = options.defaultVariationValue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// default: variable
|
|
130
|
+
if (
|
|
131
|
+
typeof options.defaultVariableValue !== "undefined" &&
|
|
132
|
+
evaluation.type === "variable" &&
|
|
133
|
+
typeof evaluation.variableValue === "undefined"
|
|
134
|
+
) {
|
|
135
|
+
evaluation.variableValue = options.defaultVariableValue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// run after hooks
|
|
139
|
+
for (const hook of hooks) {
|
|
140
|
+
if (hook.after) {
|
|
141
|
+
evaluation = hook.after(evaluation, options);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return evaluation;
|
|
146
|
+
} catch (e) {
|
|
147
|
+
const { type, featureKey, variableKey, logger } = opts;
|
|
148
|
+
|
|
149
|
+
const evaluation: Evaluation = {
|
|
150
|
+
type,
|
|
151
|
+
featureKey,
|
|
152
|
+
variableKey,
|
|
153
|
+
reason: EvaluationReason.ERROR,
|
|
154
|
+
error: e,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
logger.error("error during evaluation", evaluation);
|
|
158
|
+
|
|
159
|
+
return evaluation;
|
|
160
|
+
}
|
|
101
161
|
}
|
|
102
162
|
|
|
103
163
|
export function evaluate(options: EvaluateOptions): Evaluation {
|
|
164
|
+
const { type, featureKey, variableKey, context, logger, datafileReader, sticky, hooksManager } =
|
|
165
|
+
options;
|
|
166
|
+
|
|
167
|
+
const hooks = hooksManager.getAll();
|
|
104
168
|
let evaluation: Evaluation;
|
|
105
|
-
const {
|
|
106
|
-
type,
|
|
107
|
-
featureKey,
|
|
108
|
-
variableKey,
|
|
109
|
-
context,
|
|
110
|
-
logger,
|
|
111
|
-
datafileReader,
|
|
112
|
-
statuses,
|
|
113
|
-
stickyFeatures,
|
|
114
|
-
initialFeatures,
|
|
115
|
-
interceptContext,
|
|
116
|
-
bucketKeySeparator,
|
|
117
|
-
configureBucketKey,
|
|
118
|
-
configureBucketValue,
|
|
119
|
-
} = options;
|
|
120
169
|
|
|
121
170
|
try {
|
|
122
|
-
const key = typeof featureKey === "string" ? featureKey : featureKey.key;
|
|
123
|
-
|
|
124
171
|
/**
|
|
125
172
|
* Root flag evaluation
|
|
126
173
|
*/
|
|
@@ -128,25 +175,66 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
128
175
|
if (type !== "flag") {
|
|
129
176
|
// needed by variation and variable evaluations
|
|
130
177
|
flag = evaluate({
|
|
178
|
+
...options,
|
|
131
179
|
type: "flag",
|
|
132
|
-
featureKey: key,
|
|
133
|
-
context,
|
|
134
|
-
logger,
|
|
135
|
-
datafileReader,
|
|
136
|
-
statuses,
|
|
137
|
-
stickyFeatures,
|
|
138
|
-
initialFeatures,
|
|
139
|
-
bucketKeySeparator,
|
|
140
|
-
configureBucketKey,
|
|
141
|
-
configureBucketValue,
|
|
142
180
|
});
|
|
143
181
|
|
|
144
182
|
if (flag.enabled === false) {
|
|
145
183
|
evaluation = {
|
|
146
|
-
|
|
184
|
+
type,
|
|
185
|
+
featureKey,
|
|
147
186
|
reason: EvaluationReason.DISABLED,
|
|
148
187
|
};
|
|
149
188
|
|
|
189
|
+
const feature = datafileReader.getFeature(featureKey);
|
|
190
|
+
|
|
191
|
+
// serve variable default value if feature is disabled (if explicitly specified)
|
|
192
|
+
if (type === "variable") {
|
|
193
|
+
if (
|
|
194
|
+
feature &&
|
|
195
|
+
variableKey &&
|
|
196
|
+
feature.variablesSchema &&
|
|
197
|
+
feature.variablesSchema[variableKey]
|
|
198
|
+
) {
|
|
199
|
+
const variableSchema = feature.variablesSchema[variableKey];
|
|
200
|
+
|
|
201
|
+
if (typeof variableSchema.disabledValue !== "undefined") {
|
|
202
|
+
// disabledValue: <value>
|
|
203
|
+
evaluation = {
|
|
204
|
+
type,
|
|
205
|
+
featureKey,
|
|
206
|
+
reason: EvaluationReason.VARIABLE_DISABLED,
|
|
207
|
+
variableKey,
|
|
208
|
+
variableValue: variableSchema.disabledValue,
|
|
209
|
+
variableSchema,
|
|
210
|
+
enabled: false,
|
|
211
|
+
};
|
|
212
|
+
} else if (variableSchema.useDefaultWhenDisabled) {
|
|
213
|
+
// useDefaultWhenDisabled: true
|
|
214
|
+
evaluation = {
|
|
215
|
+
type,
|
|
216
|
+
featureKey,
|
|
217
|
+
reason: EvaluationReason.VARIABLE_DEFAULT,
|
|
218
|
+
variableKey,
|
|
219
|
+
variableValue: variableSchema.defaultValue,
|
|
220
|
+
variableSchema,
|
|
221
|
+
enabled: false,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// serve disabled variation value if feature is disabled (if explicitly specified)
|
|
228
|
+
if (type === "variation" && feature && feature.disabledVariationValue) {
|
|
229
|
+
evaluation = {
|
|
230
|
+
type,
|
|
231
|
+
featureKey,
|
|
232
|
+
reason: EvaluationReason.VARIATION_DISABLED,
|
|
233
|
+
variationValue: feature.disabledVariationValue,
|
|
234
|
+
enabled: false,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
150
238
|
logger.debug("feature is disabled", evaluation);
|
|
151
239
|
|
|
152
240
|
return evaluation;
|
|
@@ -156,14 +244,15 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
156
244
|
/**
|
|
157
245
|
* Sticky
|
|
158
246
|
*/
|
|
159
|
-
if (
|
|
247
|
+
if (sticky && sticky[featureKey]) {
|
|
160
248
|
// flag
|
|
161
|
-
if (type === "flag" && typeof
|
|
249
|
+
if (type === "flag" && typeof sticky[featureKey].enabled !== "undefined") {
|
|
162
250
|
evaluation = {
|
|
163
|
-
|
|
251
|
+
type,
|
|
252
|
+
featureKey,
|
|
164
253
|
reason: EvaluationReason.STICKY,
|
|
165
|
-
sticky:
|
|
166
|
-
enabled:
|
|
254
|
+
sticky: sticky[featureKey],
|
|
255
|
+
enabled: sticky[featureKey].enabled,
|
|
167
256
|
};
|
|
168
257
|
|
|
169
258
|
logger.debug("using sticky enabled", evaluation);
|
|
@@ -173,11 +262,12 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
173
262
|
|
|
174
263
|
// variation
|
|
175
264
|
if (type === "variation") {
|
|
176
|
-
const variationValue =
|
|
265
|
+
const variationValue = sticky[featureKey].variation;
|
|
177
266
|
|
|
178
267
|
if (typeof variationValue !== "undefined") {
|
|
179
268
|
evaluation = {
|
|
180
|
-
|
|
269
|
+
type,
|
|
270
|
+
featureKey,
|
|
181
271
|
reason: EvaluationReason.STICKY,
|
|
182
272
|
variationValue,
|
|
183
273
|
};
|
|
@@ -190,14 +280,15 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
190
280
|
|
|
191
281
|
// variable
|
|
192
282
|
if (variableKey) {
|
|
193
|
-
const variables =
|
|
283
|
+
const variables = sticky[featureKey].variables;
|
|
194
284
|
|
|
195
285
|
if (variables) {
|
|
196
286
|
const result = variables[variableKey];
|
|
197
287
|
|
|
198
288
|
if (typeof result !== "undefined") {
|
|
199
289
|
evaluation = {
|
|
200
|
-
|
|
290
|
+
type,
|
|
291
|
+
featureKey,
|
|
201
292
|
reason: EvaluationReason.STICKY,
|
|
202
293
|
variableKey,
|
|
203
294
|
variableValue: result,
|
|
@@ -211,60 +302,6 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
211
302
|
}
|
|
212
303
|
}
|
|
213
304
|
|
|
214
|
-
/**
|
|
215
|
-
* Initial
|
|
216
|
-
*/
|
|
217
|
-
if (statuses && !statuses.ready && initialFeatures && initialFeatures[key]) {
|
|
218
|
-
// flag
|
|
219
|
-
if (type === "flag" && typeof initialFeatures[key].enabled !== "undefined") {
|
|
220
|
-
evaluation = {
|
|
221
|
-
featureKey: key,
|
|
222
|
-
reason: EvaluationReason.INITIAL,
|
|
223
|
-
initial: initialFeatures[key],
|
|
224
|
-
enabled: initialFeatures[key].enabled,
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
logger.debug("using initial enabled", evaluation);
|
|
228
|
-
|
|
229
|
-
return evaluation;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// variation
|
|
233
|
-
if (type === "variation" && typeof initialFeatures[key].variation !== "undefined") {
|
|
234
|
-
const variationValue = initialFeatures[key].variation;
|
|
235
|
-
|
|
236
|
-
evaluation = {
|
|
237
|
-
featureKey: key,
|
|
238
|
-
reason: EvaluationReason.INITIAL,
|
|
239
|
-
variationValue,
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
logger.debug("using initial variation", evaluation);
|
|
243
|
-
|
|
244
|
-
return evaluation;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// variable
|
|
248
|
-
if (variableKey) {
|
|
249
|
-
const variables = initialFeatures[key].variables;
|
|
250
|
-
|
|
251
|
-
if (variables) {
|
|
252
|
-
if (typeof variables[variableKey] !== "undefined") {
|
|
253
|
-
evaluation = {
|
|
254
|
-
featureKey: key,
|
|
255
|
-
reason: EvaluationReason.INITIAL,
|
|
256
|
-
variableKey,
|
|
257
|
-
variableValue: variables[variableKey],
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
logger.debug("using initial variable", evaluation);
|
|
261
|
-
|
|
262
|
-
return evaluation;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
305
|
/**
|
|
269
306
|
* Feature
|
|
270
307
|
*/
|
|
@@ -274,8 +311,9 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
274
311
|
// feature: not found
|
|
275
312
|
if (!feature) {
|
|
276
313
|
evaluation = {
|
|
277
|
-
|
|
278
|
-
|
|
314
|
+
type,
|
|
315
|
+
featureKey,
|
|
316
|
+
reason: EvaluationReason.FEATURE_NOT_FOUND,
|
|
279
317
|
};
|
|
280
318
|
|
|
281
319
|
logger.warn("feature not found", evaluation);
|
|
@@ -285,7 +323,7 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
285
323
|
|
|
286
324
|
// feature: deprecated
|
|
287
325
|
if (type === "flag" && feature.deprecated) {
|
|
288
|
-
logger.warn("feature is deprecated", { featureKey
|
|
326
|
+
logger.warn("feature is deprecated", { featureKey });
|
|
289
327
|
}
|
|
290
328
|
|
|
291
329
|
// variableSchema
|
|
@@ -299,8 +337,9 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
299
337
|
// variable schema not found
|
|
300
338
|
if (!variableSchema) {
|
|
301
339
|
evaluation = {
|
|
302
|
-
|
|
303
|
-
|
|
340
|
+
type,
|
|
341
|
+
featureKey,
|
|
342
|
+
reason: EvaluationReason.VARIABLE_NOT_FOUND,
|
|
304
343
|
variableKey,
|
|
305
344
|
};
|
|
306
345
|
|
|
@@ -311,7 +350,7 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
311
350
|
|
|
312
351
|
if (variableSchema.deprecated) {
|
|
313
352
|
logger.warn("variable is deprecated", {
|
|
314
|
-
featureKey
|
|
353
|
+
featureKey,
|
|
315
354
|
variableKey,
|
|
316
355
|
});
|
|
317
356
|
}
|
|
@@ -320,7 +359,8 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
320
359
|
// variation: no variations
|
|
321
360
|
if (type === "variation" && (!feature.variations || feature.variations.length === 0)) {
|
|
322
361
|
evaluation = {
|
|
323
|
-
|
|
362
|
+
type,
|
|
363
|
+
featureKey,
|
|
324
364
|
reason: EvaluationReason.NO_VARIATIONS,
|
|
325
365
|
};
|
|
326
366
|
|
|
@@ -329,18 +369,17 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
329
369
|
return evaluation;
|
|
330
370
|
}
|
|
331
371
|
|
|
332
|
-
const finalContext = interceptContext ? interceptContext(context) : context;
|
|
333
|
-
|
|
334
372
|
/**
|
|
335
373
|
* Forced
|
|
336
374
|
*/
|
|
337
|
-
const { force, forceIndex } =
|
|
375
|
+
const { force, forceIndex } = datafileReader.getMatchedForce(feature, context);
|
|
338
376
|
|
|
339
377
|
if (force) {
|
|
340
378
|
// flag
|
|
341
379
|
if (type === "flag" && typeof force.enabled !== "undefined") {
|
|
342
380
|
evaluation = {
|
|
343
|
-
|
|
381
|
+
type,
|
|
382
|
+
featureKey,
|
|
344
383
|
reason: EvaluationReason.FORCED,
|
|
345
384
|
forceIndex,
|
|
346
385
|
force,
|
|
@@ -358,7 +397,8 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
358
397
|
|
|
359
398
|
if (variation) {
|
|
360
399
|
evaluation = {
|
|
361
|
-
|
|
400
|
+
type,
|
|
401
|
+
featureKey,
|
|
362
402
|
reason: EvaluationReason.FORCED,
|
|
363
403
|
forceIndex,
|
|
364
404
|
force,
|
|
@@ -374,7 +414,8 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
374
414
|
// variable
|
|
375
415
|
if (variableKey && force.variables && typeof force.variables[variableKey] !== "undefined") {
|
|
376
416
|
evaluation = {
|
|
377
|
-
|
|
417
|
+
type,
|
|
418
|
+
featureKey,
|
|
378
419
|
reason: EvaluationReason.FORCED,
|
|
379
420
|
forceIndex,
|
|
380
421
|
force,
|
|
@@ -405,17 +446,9 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
405
446
|
}
|
|
406
447
|
|
|
407
448
|
const requiredEvaluation = evaluate({
|
|
449
|
+
...options,
|
|
408
450
|
type: "flag",
|
|
409
451
|
featureKey: requiredKey,
|
|
410
|
-
context: finalContext,
|
|
411
|
-
logger,
|
|
412
|
-
datafileReader,
|
|
413
|
-
statuses,
|
|
414
|
-
stickyFeatures,
|
|
415
|
-
initialFeatures,
|
|
416
|
-
bucketKeySeparator,
|
|
417
|
-
configureBucketKey,
|
|
418
|
-
configureBucketValue,
|
|
419
452
|
});
|
|
420
453
|
const requiredIsEnabled = requiredEvaluation.enabled;
|
|
421
454
|
|
|
@@ -425,17 +458,9 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
425
458
|
|
|
426
459
|
if (typeof requiredVariation !== "undefined") {
|
|
427
460
|
const requiredVariationEvaluation = evaluate({
|
|
461
|
+
...options,
|
|
428
462
|
type: "variation",
|
|
429
463
|
featureKey: requiredKey,
|
|
430
|
-
context: finalContext,
|
|
431
|
-
logger,
|
|
432
|
-
datafileReader,
|
|
433
|
-
statuses,
|
|
434
|
-
stickyFeatures,
|
|
435
|
-
initialFeatures,
|
|
436
|
-
bucketKeySeparator,
|
|
437
|
-
configureBucketKey,
|
|
438
|
-
configureBucketValue,
|
|
439
464
|
});
|
|
440
465
|
|
|
441
466
|
let requiredVariationValue;
|
|
@@ -454,7 +479,8 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
454
479
|
|
|
455
480
|
if (!requiredFeaturesAreEnabled) {
|
|
456
481
|
evaluation = {
|
|
457
|
-
|
|
482
|
+
type,
|
|
483
|
+
featureKey,
|
|
458
484
|
reason: EvaluationReason.REQUIRED,
|
|
459
485
|
required: feature.required,
|
|
460
486
|
enabled: requiredFeaturesAreEnabled,
|
|
@@ -469,34 +495,71 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
469
495
|
/**
|
|
470
496
|
* Bucketing
|
|
471
497
|
*/
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
498
|
+
// bucketKey
|
|
499
|
+
let bucketKey = getBucketKey({
|
|
500
|
+
featureKey,
|
|
501
|
+
bucketBy: feature.bucketBy,
|
|
502
|
+
context,
|
|
503
|
+
|
|
475
504
|
logger,
|
|
476
|
-
bucketKeySeparator,
|
|
477
|
-
configureBucketKey,
|
|
478
|
-
configureBucketValue,
|
|
479
505
|
});
|
|
506
|
+
for (const hook of hooks) {
|
|
507
|
+
if (hook.bucketKey) {
|
|
508
|
+
bucketKey = hook.bucketKey({
|
|
509
|
+
featureKey,
|
|
510
|
+
context,
|
|
511
|
+
bucketBy: feature.bucketBy,
|
|
512
|
+
bucketKey,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// bucketValue
|
|
518
|
+
let bucketValue = getBucketedNumber(bucketKey);
|
|
519
|
+
|
|
520
|
+
for (const hook of hooks) {
|
|
521
|
+
if (hook.bucketValue) {
|
|
522
|
+
bucketValue = hook.bucketValue({
|
|
523
|
+
featureKey,
|
|
524
|
+
bucketKey,
|
|
525
|
+
context,
|
|
526
|
+
bucketValue,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
480
530
|
|
|
481
531
|
let matchedTraffic: Traffic | undefined;
|
|
482
532
|
let matchedAllocation: Allocation | undefined;
|
|
483
533
|
|
|
484
534
|
if (type !== "flag") {
|
|
485
|
-
|
|
486
|
-
feature.traffic,
|
|
487
|
-
finalContext,
|
|
488
|
-
bucketValue,
|
|
489
|
-
datafileReader,
|
|
490
|
-
logger,
|
|
491
|
-
);
|
|
535
|
+
matchedTraffic = datafileReader.getMatchedTraffic(feature.traffic, context);
|
|
492
536
|
|
|
493
|
-
matchedTraffic
|
|
494
|
-
|
|
537
|
+
if (matchedTraffic) {
|
|
538
|
+
matchedAllocation = datafileReader.getMatchedAllocation(matchedTraffic, bucketValue);
|
|
539
|
+
}
|
|
495
540
|
} else {
|
|
496
|
-
matchedTraffic = getMatchedTraffic(feature.traffic,
|
|
541
|
+
matchedTraffic = datafileReader.getMatchedTraffic(feature.traffic, context);
|
|
497
542
|
}
|
|
498
543
|
|
|
499
544
|
if (matchedTraffic) {
|
|
545
|
+
// percentage: 0
|
|
546
|
+
if (matchedTraffic.percentage === 0) {
|
|
547
|
+
evaluation = {
|
|
548
|
+
type,
|
|
549
|
+
featureKey,
|
|
550
|
+
reason: EvaluationReason.RULE,
|
|
551
|
+
bucketKey,
|
|
552
|
+
bucketValue,
|
|
553
|
+
ruleKey: matchedTraffic.key,
|
|
554
|
+
traffic: matchedTraffic,
|
|
555
|
+
enabled: false,
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
logger.debug("matched rule with 0 percentage", evaluation);
|
|
559
|
+
|
|
560
|
+
return evaluation;
|
|
561
|
+
}
|
|
562
|
+
|
|
500
563
|
// flag
|
|
501
564
|
if (type === "flag") {
|
|
502
565
|
// flag: check if mutually exclusive
|
|
@@ -508,7 +571,8 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
508
571
|
// matched
|
|
509
572
|
if (matchedRange) {
|
|
510
573
|
evaluation = {
|
|
511
|
-
|
|
574
|
+
type,
|
|
575
|
+
featureKey,
|
|
512
576
|
reason: EvaluationReason.ALLOCATED,
|
|
513
577
|
bucketKey,
|
|
514
578
|
bucketValue,
|
|
@@ -525,7 +589,8 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
525
589
|
|
|
526
590
|
// no match
|
|
527
591
|
evaluation = {
|
|
528
|
-
|
|
592
|
+
type,
|
|
593
|
+
featureKey,
|
|
529
594
|
reason: EvaluationReason.OUT_OF_RANGE,
|
|
530
595
|
bucketKey,
|
|
531
596
|
bucketValue,
|
|
@@ -540,8 +605,9 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
540
605
|
// flag: override from rule
|
|
541
606
|
if (typeof matchedTraffic.enabled !== "undefined") {
|
|
542
607
|
evaluation = {
|
|
543
|
-
|
|
544
|
-
|
|
608
|
+
type,
|
|
609
|
+
featureKey,
|
|
610
|
+
reason: EvaluationReason.RULE,
|
|
545
611
|
bucketKey,
|
|
546
612
|
bucketValue,
|
|
547
613
|
ruleKey: matchedTraffic.key,
|
|
@@ -557,7 +623,8 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
557
623
|
// treated as enabled because of matched traffic
|
|
558
624
|
if (bucketValue <= matchedTraffic.percentage) {
|
|
559
625
|
evaluation = {
|
|
560
|
-
|
|
626
|
+
type,
|
|
627
|
+
featureKey,
|
|
561
628
|
reason: EvaluationReason.RULE,
|
|
562
629
|
bucketKey,
|
|
563
630
|
bucketValue,
|
|
@@ -580,7 +647,8 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
580
647
|
|
|
581
648
|
if (variation) {
|
|
582
649
|
evaluation = {
|
|
583
|
-
|
|
650
|
+
type,
|
|
651
|
+
featureKey,
|
|
584
652
|
reason: EvaluationReason.RULE,
|
|
585
653
|
bucketKey,
|
|
586
654
|
bucketValue,
|
|
@@ -601,7 +669,8 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
601
669
|
|
|
602
670
|
if (variation) {
|
|
603
671
|
evaluation = {
|
|
604
|
-
|
|
672
|
+
type,
|
|
673
|
+
featureKey,
|
|
605
674
|
reason: EvaluationReason.ALLOCATED,
|
|
606
675
|
bucketKey,
|
|
607
676
|
bucketValue,
|
|
@@ -627,7 +696,8 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
627
696
|
typeof matchedTraffic.variables[variableKey] !== "undefined"
|
|
628
697
|
) {
|
|
629
698
|
evaluation = {
|
|
630
|
-
|
|
699
|
+
type,
|
|
700
|
+
featureKey,
|
|
631
701
|
reason: EvaluationReason.RULE,
|
|
632
702
|
bucketKey,
|
|
633
703
|
bucketValue,
|
|
@@ -657,70 +727,71 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
657
727
|
if (variationValue && Array.isArray(feature.variations)) {
|
|
658
728
|
const variation = feature.variations.find((v) => v.value === variationValue);
|
|
659
729
|
|
|
660
|
-
if (variation && variation.
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
if (
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
);
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
if (o.segments) {
|
|
675
|
-
return allGroupSegmentsAreMatched(
|
|
676
|
-
parseFromStringifiedSegments(o.segments),
|
|
677
|
-
finalContext,
|
|
678
|
-
datafileReader,
|
|
679
|
-
logger,
|
|
680
|
-
);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
return false;
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
if (override) {
|
|
687
|
-
evaluation = {
|
|
688
|
-
featureKey: feature.key,
|
|
689
|
-
reason: EvaluationReason.OVERRIDE,
|
|
690
|
-
bucketKey,
|
|
691
|
-
bucketValue,
|
|
692
|
-
ruleKey: matchedTraffic?.key,
|
|
693
|
-
traffic: matchedTraffic,
|
|
694
|
-
variableKey,
|
|
695
|
-
variableSchema,
|
|
696
|
-
variableValue: override.value,
|
|
697
|
-
};
|
|
698
|
-
|
|
699
|
-
logger.debug("variable override", evaluation);
|
|
700
|
-
|
|
701
|
-
return evaluation;
|
|
702
|
-
}
|
|
730
|
+
if (variation && variation.variableOverrides && variation.variableOverrides[variableKey]) {
|
|
731
|
+
const overrides = variation.variableOverrides[variableKey];
|
|
732
|
+
|
|
733
|
+
const override = overrides.find((o) => {
|
|
734
|
+
if (o.conditions) {
|
|
735
|
+
return datafileReader.allConditionsAreMatched(
|
|
736
|
+
typeof o.conditions === "string" && o.conditions !== "*"
|
|
737
|
+
? JSON.parse(o.conditions)
|
|
738
|
+
: o.conditions,
|
|
739
|
+
context,
|
|
740
|
+
);
|
|
703
741
|
}
|
|
704
742
|
|
|
705
|
-
if (
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
ruleKey: matchedTraffic?.key,
|
|
712
|
-
traffic: matchedTraffic,
|
|
713
|
-
variableKey,
|
|
714
|
-
variableSchema,
|
|
715
|
-
variableValue: variableFromVariation.value,
|
|
716
|
-
};
|
|
743
|
+
if (o.segments) {
|
|
744
|
+
return datafileReader.allSegmentsAreMatched(
|
|
745
|
+
datafileReader.parseSegmentsIfStringified(o.segments),
|
|
746
|
+
context,
|
|
747
|
+
);
|
|
748
|
+
}
|
|
717
749
|
|
|
718
|
-
|
|
750
|
+
return false;
|
|
751
|
+
});
|
|
719
752
|
|
|
720
|
-
|
|
721
|
-
|
|
753
|
+
if (override) {
|
|
754
|
+
evaluation = {
|
|
755
|
+
type,
|
|
756
|
+
featureKey,
|
|
757
|
+
reason: EvaluationReason.VARIABLE_OVERRIDE,
|
|
758
|
+
bucketKey,
|
|
759
|
+
bucketValue,
|
|
760
|
+
ruleKey: matchedTraffic?.key,
|
|
761
|
+
traffic: matchedTraffic,
|
|
762
|
+
variableKey,
|
|
763
|
+
variableSchema,
|
|
764
|
+
variableValue: override.value,
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
logger.debug("variable override", evaluation);
|
|
768
|
+
|
|
769
|
+
return evaluation;
|
|
722
770
|
}
|
|
723
771
|
}
|
|
772
|
+
|
|
773
|
+
if (
|
|
774
|
+
variation &&
|
|
775
|
+
variation.variables &&
|
|
776
|
+
typeof variation.variables[variableKey] !== "undefined"
|
|
777
|
+
) {
|
|
778
|
+
evaluation = {
|
|
779
|
+
type,
|
|
780
|
+
featureKey,
|
|
781
|
+
reason: EvaluationReason.ALLOCATED,
|
|
782
|
+
bucketKey,
|
|
783
|
+
bucketValue,
|
|
784
|
+
ruleKey: matchedTraffic?.key,
|
|
785
|
+
traffic: matchedTraffic,
|
|
786
|
+
variableKey,
|
|
787
|
+
variableSchema,
|
|
788
|
+
variableValue: variation.variables[variableKey],
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
logger.debug("allocated variable", evaluation);
|
|
792
|
+
|
|
793
|
+
return evaluation;
|
|
794
|
+
}
|
|
724
795
|
}
|
|
725
796
|
}
|
|
726
797
|
|
|
@@ -729,7 +800,8 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
729
800
|
*/
|
|
730
801
|
if (type === "variation") {
|
|
731
802
|
evaluation = {
|
|
732
|
-
|
|
803
|
+
type,
|
|
804
|
+
featureKey,
|
|
733
805
|
reason: EvaluationReason.NO_MATCH,
|
|
734
806
|
bucketKey,
|
|
735
807
|
bucketValue,
|
|
@@ -740,24 +812,41 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
740
812
|
return evaluation;
|
|
741
813
|
}
|
|
742
814
|
|
|
743
|
-
if (type === "variable"
|
|
815
|
+
if (type === "variable") {
|
|
816
|
+
if (variableSchema) {
|
|
817
|
+
evaluation = {
|
|
818
|
+
type,
|
|
819
|
+
featureKey,
|
|
820
|
+
reason: EvaluationReason.VARIABLE_DEFAULT,
|
|
821
|
+
bucketKey,
|
|
822
|
+
bucketValue,
|
|
823
|
+
variableKey,
|
|
824
|
+
variableSchema,
|
|
825
|
+
variableValue: variableSchema.defaultValue,
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
logger.debug("using default value", evaluation);
|
|
829
|
+
|
|
830
|
+
return evaluation;
|
|
831
|
+
}
|
|
832
|
+
|
|
744
833
|
evaluation = {
|
|
745
|
-
|
|
746
|
-
|
|
834
|
+
type,
|
|
835
|
+
featureKey,
|
|
836
|
+
reason: EvaluationReason.VARIABLE_NOT_FOUND,
|
|
837
|
+
variableKey,
|
|
747
838
|
bucketKey,
|
|
748
839
|
bucketValue,
|
|
749
|
-
variableKey,
|
|
750
|
-
variableSchema,
|
|
751
|
-
variableValue: variableSchema.defaultValue,
|
|
752
840
|
};
|
|
753
841
|
|
|
754
|
-
logger.debug("
|
|
842
|
+
logger.debug("variable not found", evaluation);
|
|
755
843
|
|
|
756
844
|
return evaluation;
|
|
757
845
|
}
|
|
758
846
|
|
|
759
847
|
evaluation = {
|
|
760
|
-
|
|
848
|
+
type,
|
|
849
|
+
featureKey,
|
|
761
850
|
reason: EvaluationReason.NO_MATCH,
|
|
762
851
|
bucketKey,
|
|
763
852
|
bucketValue,
|
|
@@ -769,7 +858,9 @@ export function evaluate(options: EvaluateOptions): Evaluation {
|
|
|
769
858
|
return evaluation;
|
|
770
859
|
} catch (e) {
|
|
771
860
|
evaluation = {
|
|
772
|
-
|
|
861
|
+
type,
|
|
862
|
+
featureKey,
|
|
863
|
+
variableKey,
|
|
773
864
|
reason: EvaluationReason.ERROR,
|
|
774
865
|
error: e,
|
|
775
866
|
};
|