@featurevisor/sdk 1.29.3 → 1.30.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 +11 -0
- package/coverage/clover.xml +410 -413
- package/coverage/coverage-final.json +3 -2
- package/coverage/lcov-report/bucket.ts.html +387 -9
- package/coverage/lcov-report/conditions.ts.html +1 -1
- package/coverage/lcov-report/datafileReader.ts.html +1 -1
- package/coverage/lcov-report/emitter.ts.html +1 -1
- package/coverage/lcov-report/evaluate.ts.html +2401 -0
- package/coverage/lcov-report/feature.ts.html +1 -1
- package/coverage/lcov-report/index.html +44 -29
- package/coverage/lcov-report/instance.ts.html +80 -2396
- package/coverage/lcov-report/logger.ts.html +1 -1
- package/coverage/lcov-report/segments.ts.html +1 -1
- package/coverage/lcov.info +739 -722
- package/dist/bucket.d.ts +28 -0
- package/dist/evaluate.d.ts +59 -0
- package/dist/index.d.ts +1 -0
- 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 +3 -44
- package/lib/bucket.d.ts +28 -0
- package/lib/evaluate.d.ts +59 -0
- package/lib/index.d.ts +1 -0
- package/lib/instance.d.ts +3 -44
- package/package.json +2 -2
- package/src/bucket.ts +126 -0
- package/src/evaluate.ts +772 -0
- package/src/index.ts +1 -0
- package/src/instance.ts +45 -817
package/src/evaluate.ts
ADDED
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Feature,
|
|
3
|
+
FeatureKey,
|
|
4
|
+
Context,
|
|
5
|
+
BucketKey,
|
|
6
|
+
BucketValue,
|
|
7
|
+
RuleKey,
|
|
8
|
+
Traffic,
|
|
9
|
+
Force,
|
|
10
|
+
Required,
|
|
11
|
+
OverrideFeature,
|
|
12
|
+
Variation,
|
|
13
|
+
VariationValue,
|
|
14
|
+
VariableKey,
|
|
15
|
+
VariableValue,
|
|
16
|
+
VariableSchema,
|
|
17
|
+
StickyFeatures,
|
|
18
|
+
InitialFeatures,
|
|
19
|
+
Allocation,
|
|
20
|
+
} from "@featurevisor/types";
|
|
21
|
+
|
|
22
|
+
import { Logger } from "./logger";
|
|
23
|
+
import { DatafileReader } from "./datafileReader";
|
|
24
|
+
import { getBucket, ConfigureBucketKey, ConfigureBucketValue } from "./bucket";
|
|
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";
|
|
34
|
+
|
|
35
|
+
export enum EvaluationReason {
|
|
36
|
+
NOT_FOUND = "not_found",
|
|
37
|
+
NO_VARIATIONS = "no_variations",
|
|
38
|
+
NO_MATCH = "no_match",
|
|
39
|
+
DISABLED = "disabled",
|
|
40
|
+
REQUIRED = "required",
|
|
41
|
+
OUT_OF_RANGE = "out_of_range",
|
|
42
|
+
FORCED = "forced",
|
|
43
|
+
INITIAL = "initial",
|
|
44
|
+
STICKY = "sticky",
|
|
45
|
+
RULE = "rule",
|
|
46
|
+
ALLOCATED = "allocated",
|
|
47
|
+
DEFAULTED = "defaulted",
|
|
48
|
+
OVERRIDE = "override",
|
|
49
|
+
ERROR = "error",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type EvaluationType = "flag" | "variation" | "variable";
|
|
53
|
+
|
|
54
|
+
export interface Evaluation {
|
|
55
|
+
// required
|
|
56
|
+
// type: EvaluationType; // @TODO: bring in later
|
|
57
|
+
featureKey: FeatureKey;
|
|
58
|
+
reason: EvaluationReason;
|
|
59
|
+
|
|
60
|
+
// common
|
|
61
|
+
bucketKey?: BucketKey;
|
|
62
|
+
bucketValue?: BucketValue;
|
|
63
|
+
ruleKey?: RuleKey;
|
|
64
|
+
error?: Error;
|
|
65
|
+
enabled?: boolean;
|
|
66
|
+
traffic?: Traffic;
|
|
67
|
+
forceIndex?: number;
|
|
68
|
+
force?: Force;
|
|
69
|
+
required?: Required[];
|
|
70
|
+
sticky?: OverrideFeature;
|
|
71
|
+
initial?: OverrideFeature;
|
|
72
|
+
|
|
73
|
+
// variation
|
|
74
|
+
variation?: Variation;
|
|
75
|
+
variationValue?: VariationValue;
|
|
76
|
+
|
|
77
|
+
// variable
|
|
78
|
+
variableKey?: VariableKey;
|
|
79
|
+
variableValue?: VariableValue;
|
|
80
|
+
variableSchema?: VariableSchema;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface EvaluateOptions {
|
|
84
|
+
type: EvaluationType;
|
|
85
|
+
|
|
86
|
+
featureKey: FeatureKey | Feature;
|
|
87
|
+
variableKey?: VariableKey;
|
|
88
|
+
context: Context;
|
|
89
|
+
|
|
90
|
+
logger: Logger;
|
|
91
|
+
datafileReader: DatafileReader;
|
|
92
|
+
statuses?: Statuses;
|
|
93
|
+
interceptContext?: InterceptContext;
|
|
94
|
+
|
|
95
|
+
stickyFeatures?: StickyFeatures;
|
|
96
|
+
initialFeatures?: InitialFeatures;
|
|
97
|
+
|
|
98
|
+
bucketKeySeparator?: string;
|
|
99
|
+
configureBucketKey?: ConfigureBucketKey;
|
|
100
|
+
configureBucketValue?: ConfigureBucketValue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function evaluate(options: EvaluateOptions): Evaluation {
|
|
104
|
+
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
|
+
|
|
121
|
+
try {
|
|
122
|
+
const key = typeof featureKey === "string" ? featureKey : featureKey.key;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Root flag evaluation
|
|
126
|
+
*/
|
|
127
|
+
let flag: Evaluation;
|
|
128
|
+
if (type !== "flag") {
|
|
129
|
+
// needed by variation and variable evaluations
|
|
130
|
+
flag = evaluate({
|
|
131
|
+
type: "flag",
|
|
132
|
+
featureKey: key,
|
|
133
|
+
context,
|
|
134
|
+
logger,
|
|
135
|
+
datafileReader,
|
|
136
|
+
statuses,
|
|
137
|
+
stickyFeatures,
|
|
138
|
+
initialFeatures,
|
|
139
|
+
bucketKeySeparator,
|
|
140
|
+
configureBucketKey,
|
|
141
|
+
configureBucketValue,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (flag.enabled === false) {
|
|
145
|
+
evaluation = {
|
|
146
|
+
featureKey: key,
|
|
147
|
+
reason: EvaluationReason.DISABLED,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
logger.debug("feature is disabled", evaluation);
|
|
151
|
+
|
|
152
|
+
return evaluation;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Sticky
|
|
158
|
+
*/
|
|
159
|
+
if (stickyFeatures && stickyFeatures[key]) {
|
|
160
|
+
// flag
|
|
161
|
+
if (type === "flag" && typeof stickyFeatures[key].enabled !== "undefined") {
|
|
162
|
+
evaluation = {
|
|
163
|
+
featureKey: key,
|
|
164
|
+
reason: EvaluationReason.STICKY,
|
|
165
|
+
sticky: stickyFeatures[key],
|
|
166
|
+
enabled: stickyFeatures[key].enabled,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
logger.debug("using sticky enabled", evaluation);
|
|
170
|
+
|
|
171
|
+
return evaluation;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// variation
|
|
175
|
+
if (type === "variation") {
|
|
176
|
+
const variationValue = stickyFeatures[key].variation;
|
|
177
|
+
|
|
178
|
+
if (typeof variationValue !== "undefined") {
|
|
179
|
+
evaluation = {
|
|
180
|
+
featureKey: key,
|
|
181
|
+
reason: EvaluationReason.STICKY,
|
|
182
|
+
variationValue,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
logger.debug("using sticky variation", evaluation);
|
|
186
|
+
|
|
187
|
+
return evaluation;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// variable
|
|
192
|
+
if (variableKey) {
|
|
193
|
+
const variables = stickyFeatures[key].variables;
|
|
194
|
+
|
|
195
|
+
if (variables) {
|
|
196
|
+
const result = variables[variableKey];
|
|
197
|
+
|
|
198
|
+
if (typeof result !== "undefined") {
|
|
199
|
+
evaluation = {
|
|
200
|
+
featureKey: key,
|
|
201
|
+
reason: EvaluationReason.STICKY,
|
|
202
|
+
variableKey,
|
|
203
|
+
variableValue: result,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
logger.debug("using sticky variable", evaluation);
|
|
207
|
+
|
|
208
|
+
return evaluation;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
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
|
+
/**
|
|
269
|
+
* Feature
|
|
270
|
+
*/
|
|
271
|
+
const feature =
|
|
272
|
+
typeof featureKey === "string" ? datafileReader.getFeature(featureKey) : featureKey;
|
|
273
|
+
|
|
274
|
+
// feature: not found
|
|
275
|
+
if (!feature) {
|
|
276
|
+
evaluation = {
|
|
277
|
+
featureKey: key,
|
|
278
|
+
reason: EvaluationReason.NOT_FOUND, // @TODO: make it type-specific
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
logger.warn("feature not found", evaluation);
|
|
282
|
+
|
|
283
|
+
return evaluation;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// feature: deprecated
|
|
287
|
+
if (type === "flag" && feature.deprecated) {
|
|
288
|
+
logger.warn("feature is deprecated", { featureKey: feature.key });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// variableSchema
|
|
292
|
+
let variableSchema: VariableSchema | undefined;
|
|
293
|
+
|
|
294
|
+
if (variableKey) {
|
|
295
|
+
variableSchema = Array.isArray(feature.variablesSchema)
|
|
296
|
+
? feature.variablesSchema.find((v) => v.key === variableKey)
|
|
297
|
+
: undefined;
|
|
298
|
+
|
|
299
|
+
// variable schema not found
|
|
300
|
+
if (!variableSchema) {
|
|
301
|
+
evaluation = {
|
|
302
|
+
featureKey: key,
|
|
303
|
+
reason: EvaluationReason.NOT_FOUND,
|
|
304
|
+
variableKey,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
logger.warn("variable schema not found", evaluation);
|
|
308
|
+
|
|
309
|
+
return evaluation;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// variation: no variations
|
|
314
|
+
if (type === "variation" && (!feature.variations || feature.variations.length === 0)) {
|
|
315
|
+
evaluation = {
|
|
316
|
+
featureKey: key,
|
|
317
|
+
reason: EvaluationReason.NO_VARIATIONS,
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
logger.warn("no variations", evaluation);
|
|
321
|
+
|
|
322
|
+
return evaluation;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const finalContext = interceptContext ? interceptContext(context) : context;
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Forced
|
|
329
|
+
*/
|
|
330
|
+
const { force, forceIndex } = findForceFromFeature(feature, context, datafileReader, logger);
|
|
331
|
+
|
|
332
|
+
if (force) {
|
|
333
|
+
// flag
|
|
334
|
+
if (type === "flag" && typeof force.enabled !== "undefined") {
|
|
335
|
+
evaluation = {
|
|
336
|
+
featureKey: feature.key,
|
|
337
|
+
reason: EvaluationReason.FORCED,
|
|
338
|
+
forceIndex,
|
|
339
|
+
force,
|
|
340
|
+
enabled: force.enabled,
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
logger.debug("forced enabled found", evaluation);
|
|
344
|
+
|
|
345
|
+
return evaluation;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// variation
|
|
349
|
+
if (type === "variation" && force.variation && feature.variations) {
|
|
350
|
+
const variation = feature.variations.find((v) => v.value === force.variation);
|
|
351
|
+
|
|
352
|
+
if (variation) {
|
|
353
|
+
evaluation = {
|
|
354
|
+
featureKey: feature.key,
|
|
355
|
+
reason: EvaluationReason.FORCED,
|
|
356
|
+
forceIndex,
|
|
357
|
+
force,
|
|
358
|
+
variation,
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
logger.debug("forced variation found", evaluation);
|
|
362
|
+
|
|
363
|
+
return evaluation;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// variable
|
|
368
|
+
if (variableKey && force.variables && typeof force.variables[variableKey] !== "undefined") {
|
|
369
|
+
evaluation = {
|
|
370
|
+
featureKey: feature.key,
|
|
371
|
+
reason: EvaluationReason.FORCED,
|
|
372
|
+
forceIndex,
|
|
373
|
+
force,
|
|
374
|
+
variableKey,
|
|
375
|
+
variableSchema,
|
|
376
|
+
variableValue: force.variables[variableKey],
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
logger.debug("forced variable", evaluation);
|
|
380
|
+
|
|
381
|
+
return evaluation;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Required
|
|
387
|
+
*/
|
|
388
|
+
if (type === "flag" && feature.required && feature.required.length > 0) {
|
|
389
|
+
const requiredFeaturesAreEnabled = feature.required.every((required) => {
|
|
390
|
+
let requiredKey;
|
|
391
|
+
let requiredVariation;
|
|
392
|
+
|
|
393
|
+
if (typeof required === "string") {
|
|
394
|
+
requiredKey = required;
|
|
395
|
+
} else {
|
|
396
|
+
requiredKey = required.key;
|
|
397
|
+
requiredVariation = required.variation;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const requiredEvaluation = evaluate({
|
|
401
|
+
type: "flag",
|
|
402
|
+
featureKey: requiredKey,
|
|
403
|
+
context: finalContext,
|
|
404
|
+
logger,
|
|
405
|
+
datafileReader,
|
|
406
|
+
statuses,
|
|
407
|
+
stickyFeatures,
|
|
408
|
+
initialFeatures,
|
|
409
|
+
bucketKeySeparator,
|
|
410
|
+
configureBucketKey,
|
|
411
|
+
configureBucketValue,
|
|
412
|
+
});
|
|
413
|
+
const requiredIsEnabled = requiredEvaluation.enabled;
|
|
414
|
+
|
|
415
|
+
if (!requiredIsEnabled) {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (typeof requiredVariation !== "undefined") {
|
|
420
|
+
const requiredVariationEvaluation = evaluate({
|
|
421
|
+
type: "variation",
|
|
422
|
+
featureKey: requiredKey,
|
|
423
|
+
context: finalContext,
|
|
424
|
+
logger,
|
|
425
|
+
datafileReader,
|
|
426
|
+
statuses,
|
|
427
|
+
stickyFeatures,
|
|
428
|
+
initialFeatures,
|
|
429
|
+
bucketKeySeparator,
|
|
430
|
+
configureBucketKey,
|
|
431
|
+
configureBucketValue,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
let requiredVariationValue;
|
|
435
|
+
|
|
436
|
+
if (requiredVariationEvaluation.variationValue) {
|
|
437
|
+
requiredVariationValue = requiredVariationEvaluation.variationValue;
|
|
438
|
+
} else if (requiredVariationEvaluation.variation) {
|
|
439
|
+
requiredVariationValue = requiredVariationEvaluation.variation.value;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return requiredVariationValue === requiredVariation;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return true;
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
if (!requiredFeaturesAreEnabled) {
|
|
449
|
+
evaluation = {
|
|
450
|
+
featureKey: feature.key,
|
|
451
|
+
reason: EvaluationReason.REQUIRED,
|
|
452
|
+
required: feature.required,
|
|
453
|
+
enabled: requiredFeaturesAreEnabled,
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
logger.debug("required features not enabled", evaluation);
|
|
457
|
+
|
|
458
|
+
return evaluation;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Bucketing
|
|
464
|
+
*/
|
|
465
|
+
const { bucketKey, bucketValue } = getBucket({
|
|
466
|
+
feature,
|
|
467
|
+
context: finalContext,
|
|
468
|
+
logger,
|
|
469
|
+
bucketKeySeparator,
|
|
470
|
+
configureBucketKey,
|
|
471
|
+
configureBucketValue,
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
let matchedTraffic: Traffic | undefined;
|
|
475
|
+
let matchedAllocation: Allocation | undefined;
|
|
476
|
+
|
|
477
|
+
if (type !== "flag") {
|
|
478
|
+
const matched = getMatchedTrafficAndAllocation(
|
|
479
|
+
feature.traffic,
|
|
480
|
+
finalContext,
|
|
481
|
+
bucketValue,
|
|
482
|
+
datafileReader,
|
|
483
|
+
logger,
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
matchedTraffic = matched.matchedTraffic;
|
|
487
|
+
matchedAllocation = matched.matchedAllocation;
|
|
488
|
+
} else {
|
|
489
|
+
matchedTraffic = getMatchedTraffic(feature.traffic, finalContext, datafileReader, logger);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (matchedTraffic) {
|
|
493
|
+
// flag
|
|
494
|
+
if (type === "flag") {
|
|
495
|
+
// flag: check if mutually exclusive
|
|
496
|
+
if (feature.ranges && feature.ranges.length > 0) {
|
|
497
|
+
const matchedRange = feature.ranges.find((range) => {
|
|
498
|
+
return bucketValue >= range[0] && bucketValue < range[1];
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// matched
|
|
502
|
+
if (matchedRange) {
|
|
503
|
+
evaluation = {
|
|
504
|
+
featureKey: feature.key,
|
|
505
|
+
reason: EvaluationReason.ALLOCATED,
|
|
506
|
+
bucketKey,
|
|
507
|
+
bucketValue,
|
|
508
|
+
ruleKey: matchedTraffic.key,
|
|
509
|
+
traffic: matchedTraffic,
|
|
510
|
+
enabled:
|
|
511
|
+
typeof matchedTraffic.enabled === "undefined" ? true : matchedTraffic.enabled,
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
logger.debug("matched", evaluation);
|
|
515
|
+
|
|
516
|
+
return evaluation;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// no match
|
|
520
|
+
evaluation = {
|
|
521
|
+
featureKey: feature.key,
|
|
522
|
+
reason: EvaluationReason.OUT_OF_RANGE,
|
|
523
|
+
bucketKey,
|
|
524
|
+
bucketValue,
|
|
525
|
+
enabled: false,
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
logger.debug("not matched", evaluation);
|
|
529
|
+
|
|
530
|
+
return evaluation;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// flag: override from rule
|
|
534
|
+
if (typeof matchedTraffic.enabled !== "undefined") {
|
|
535
|
+
evaluation = {
|
|
536
|
+
featureKey: feature.key,
|
|
537
|
+
reason: EvaluationReason.OVERRIDE,
|
|
538
|
+
bucketKey,
|
|
539
|
+
bucketValue,
|
|
540
|
+
ruleKey: matchedTraffic.key,
|
|
541
|
+
traffic: matchedTraffic,
|
|
542
|
+
enabled: matchedTraffic.enabled,
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
logger.debug("override from rule", evaluation);
|
|
546
|
+
|
|
547
|
+
return evaluation;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// treated as enabled because of matched traffic
|
|
551
|
+
if (bucketValue <= matchedTraffic.percentage) {
|
|
552
|
+
evaluation = {
|
|
553
|
+
featureKey: feature.key,
|
|
554
|
+
reason: EvaluationReason.RULE,
|
|
555
|
+
bucketKey,
|
|
556
|
+
bucketValue,
|
|
557
|
+
ruleKey: matchedTraffic.key,
|
|
558
|
+
traffic: matchedTraffic,
|
|
559
|
+
enabled: true,
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
logger.debug("matched traffic", evaluation);
|
|
563
|
+
|
|
564
|
+
return evaluation;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// variation
|
|
569
|
+
if (type === "variation" && feature.variations) {
|
|
570
|
+
// override from rule
|
|
571
|
+
if (matchedTraffic.variation) {
|
|
572
|
+
const variation = feature.variations.find((v) => v.value === matchedTraffic.variation);
|
|
573
|
+
|
|
574
|
+
if (variation) {
|
|
575
|
+
evaluation = {
|
|
576
|
+
featureKey: feature.key,
|
|
577
|
+
reason: EvaluationReason.RULE,
|
|
578
|
+
bucketKey,
|
|
579
|
+
bucketValue,
|
|
580
|
+
ruleKey: matchedTraffic.key,
|
|
581
|
+
traffic: matchedTraffic,
|
|
582
|
+
variation,
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
logger.debug("override from rule", evaluation);
|
|
586
|
+
|
|
587
|
+
return evaluation;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// regular allocation
|
|
592
|
+
if (matchedAllocation && matchedAllocation.variation) {
|
|
593
|
+
const variation = feature.variations.find((v) => v.value === matchedAllocation.variation);
|
|
594
|
+
|
|
595
|
+
if (variation) {
|
|
596
|
+
evaluation = {
|
|
597
|
+
featureKey: feature.key,
|
|
598
|
+
reason: EvaluationReason.ALLOCATED,
|
|
599
|
+
bucketKey,
|
|
600
|
+
bucketValue,
|
|
601
|
+
ruleKey: matchedTraffic.key,
|
|
602
|
+
traffic: matchedTraffic,
|
|
603
|
+
variation,
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
logger.debug("allocated variation", evaluation);
|
|
607
|
+
|
|
608
|
+
return evaluation;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// variable
|
|
615
|
+
if (type === "variable" && variableKey) {
|
|
616
|
+
// override from rule
|
|
617
|
+
if (
|
|
618
|
+
matchedTraffic &&
|
|
619
|
+
matchedTraffic.variables &&
|
|
620
|
+
typeof matchedTraffic.variables[variableKey] !== "undefined"
|
|
621
|
+
) {
|
|
622
|
+
evaluation = {
|
|
623
|
+
featureKey: feature.key,
|
|
624
|
+
reason: EvaluationReason.RULE,
|
|
625
|
+
bucketKey,
|
|
626
|
+
bucketValue,
|
|
627
|
+
ruleKey: matchedTraffic.key,
|
|
628
|
+
traffic: matchedTraffic,
|
|
629
|
+
variableKey,
|
|
630
|
+
variableSchema,
|
|
631
|
+
variableValue: matchedTraffic.variables[variableKey],
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
logger.debug("override from rule", evaluation);
|
|
635
|
+
|
|
636
|
+
return evaluation;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// check variations
|
|
640
|
+
let variationValue;
|
|
641
|
+
|
|
642
|
+
if (force && force.variation) {
|
|
643
|
+
variationValue = force.variation;
|
|
644
|
+
} else if (matchedAllocation && matchedAllocation.variation) {
|
|
645
|
+
variationValue = matchedAllocation.variation;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (variationValue && Array.isArray(feature.variations)) {
|
|
649
|
+
const variation = feature.variations.find((v) => v.value === variationValue);
|
|
650
|
+
|
|
651
|
+
if (variation && variation.variables) {
|
|
652
|
+
const variableFromVariation = variation.variables.find((v) => v.key === variableKey);
|
|
653
|
+
|
|
654
|
+
if (variableFromVariation) {
|
|
655
|
+
if (variableFromVariation.overrides) {
|
|
656
|
+
const override = variableFromVariation.overrides.find((o) => {
|
|
657
|
+
if (o.conditions) {
|
|
658
|
+
return allConditionsAreMatched(
|
|
659
|
+
typeof o.conditions === "string" ? JSON.parse(o.conditions) : o.conditions,
|
|
660
|
+
finalContext,
|
|
661
|
+
logger,
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (o.segments) {
|
|
666
|
+
return allGroupSegmentsAreMatched(
|
|
667
|
+
parseFromStringifiedSegments(o.segments),
|
|
668
|
+
finalContext,
|
|
669
|
+
datafileReader,
|
|
670
|
+
logger,
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return false;
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
if (override) {
|
|
678
|
+
evaluation = {
|
|
679
|
+
featureKey: feature.key,
|
|
680
|
+
reason: EvaluationReason.OVERRIDE,
|
|
681
|
+
bucketKey,
|
|
682
|
+
bucketValue,
|
|
683
|
+
ruleKey: matchedTraffic?.key,
|
|
684
|
+
traffic: matchedTraffic,
|
|
685
|
+
variableKey,
|
|
686
|
+
variableSchema,
|
|
687
|
+
variableValue: override.value,
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
logger.debug("variable override", evaluation);
|
|
691
|
+
|
|
692
|
+
return evaluation;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (typeof variableFromVariation.value !== "undefined") {
|
|
697
|
+
evaluation = {
|
|
698
|
+
featureKey: feature.key,
|
|
699
|
+
reason: EvaluationReason.ALLOCATED,
|
|
700
|
+
bucketKey,
|
|
701
|
+
bucketValue,
|
|
702
|
+
ruleKey: matchedTraffic?.key,
|
|
703
|
+
traffic: matchedTraffic,
|
|
704
|
+
variableKey,
|
|
705
|
+
variableSchema,
|
|
706
|
+
variableValue: variableFromVariation.value,
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
logger.debug("allocated variable", evaluation);
|
|
710
|
+
|
|
711
|
+
return evaluation;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Nothing matched
|
|
720
|
+
*/
|
|
721
|
+
if (type === "variation") {
|
|
722
|
+
evaluation = {
|
|
723
|
+
featureKey: feature.key,
|
|
724
|
+
reason: EvaluationReason.NO_MATCH,
|
|
725
|
+
bucketKey,
|
|
726
|
+
bucketValue,
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
logger.debug("no matched variation", evaluation);
|
|
730
|
+
|
|
731
|
+
return evaluation;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (type === "variable" && variableSchema) {
|
|
735
|
+
evaluation = {
|
|
736
|
+
featureKey: feature.key,
|
|
737
|
+
reason: EvaluationReason.DEFAULTED,
|
|
738
|
+
bucketKey,
|
|
739
|
+
bucketValue,
|
|
740
|
+
variableKey,
|
|
741
|
+
variableSchema,
|
|
742
|
+
variableValue: variableSchema.defaultValue,
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
logger.debug("using default value", evaluation);
|
|
746
|
+
|
|
747
|
+
return evaluation;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
evaluation = {
|
|
751
|
+
featureKey: feature.key,
|
|
752
|
+
reason: EvaluationReason.NO_MATCH,
|
|
753
|
+
bucketKey,
|
|
754
|
+
bucketValue,
|
|
755
|
+
enabled: false,
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
logger.debug("nothing matched", evaluation);
|
|
759
|
+
|
|
760
|
+
return evaluation;
|
|
761
|
+
} catch (e) {
|
|
762
|
+
evaluation = {
|
|
763
|
+
featureKey: typeof featureKey === "string" ? featureKey : featureKey.key,
|
|
764
|
+
reason: EvaluationReason.ERROR,
|
|
765
|
+
error: e,
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
logger.error("error", evaluation);
|
|
769
|
+
|
|
770
|
+
return evaluation;
|
|
771
|
+
}
|
|
772
|
+
}
|