@featurevisor/sdk 1.29.2 → 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 +22 -0
- package/coverage/clover.xml +436 -436
- package/coverage/coverage-final.json +4 -3
- 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 +27 -9
- package/coverage/lcov-report/index.html +51 -36
- 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 +782 -759
- 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/feature.ts +7 -1
- package/src/index.ts +1 -0
- package/src/instance.ts +45 -817
package/src/instance.ts
CHANGED
|
@@ -1,38 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Context,
|
|
3
|
-
AttributeValue,
|
|
4
|
-
BucketKey,
|
|
5
|
-
BucketValue,
|
|
6
3
|
DatafileContent,
|
|
7
4
|
Feature,
|
|
8
5
|
FeatureKey,
|
|
9
6
|
InitialFeatures,
|
|
10
|
-
OverrideFeature,
|
|
11
7
|
StickyFeatures,
|
|
12
|
-
Traffic,
|
|
13
8
|
VariableType,
|
|
14
9
|
VariableValue,
|
|
15
10
|
VariationValue,
|
|
16
|
-
Variation,
|
|
17
|
-
RuleKey,
|
|
18
11
|
VariableKey,
|
|
19
|
-
VariableSchema,
|
|
20
|
-
Force,
|
|
21
|
-
Required,
|
|
22
12
|
} from "@featurevisor/types";
|
|
23
13
|
|
|
24
14
|
import { createLogger, Logger, LogLevel } from "./logger";
|
|
25
15
|
import { DatafileReader } from "./datafileReader";
|
|
26
16
|
import { Emitter } from "./emitter";
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
findForceFromFeature,
|
|
30
|
-
getMatchedTraffic,
|
|
31
|
-
getMatchedTrafficAndAllocation,
|
|
32
|
-
parseFromStringifiedSegments,
|
|
33
|
-
} from "./feature";
|
|
34
|
-
import { allConditionsAreMatched } from "./conditions";
|
|
35
|
-
import { allGroupSegmentsAreMatched } from "./segments";
|
|
17
|
+
import { ConfigureBucketKey, ConfigureBucketValue } from "./bucket";
|
|
18
|
+
import { Evaluation, evaluate } from "./evaluate";
|
|
36
19
|
|
|
37
20
|
export type ReadyCallback = () => void;
|
|
38
21
|
|
|
@@ -43,10 +26,6 @@ export type ActivationCallback = (
|
|
|
43
26
|
captureContext: Context,
|
|
44
27
|
) => void;
|
|
45
28
|
|
|
46
|
-
export type ConfigureBucketKey = (feature, context, bucketKey: BucketKey) => BucketKey;
|
|
47
|
-
|
|
48
|
-
export type ConfigureBucketValue = (feature, context, bucketValue: BucketValue) => BucketValue;
|
|
49
|
-
|
|
50
29
|
export interface Statuses {
|
|
51
30
|
ready: boolean;
|
|
52
31
|
refreshInProgress: boolean;
|
|
@@ -84,51 +63,6 @@ const emptyDatafile: DatafileContent = {
|
|
|
84
63
|
|
|
85
64
|
export type DatafileFetchHandler = (datafileUrl: string) => Promise<DatafileContent>;
|
|
86
65
|
|
|
87
|
-
export enum EvaluationReason {
|
|
88
|
-
NOT_FOUND = "not_found",
|
|
89
|
-
NO_VARIATIONS = "no_variations",
|
|
90
|
-
NO_MATCH = "no_match",
|
|
91
|
-
DISABLED = "disabled",
|
|
92
|
-
REQUIRED = "required",
|
|
93
|
-
OUT_OF_RANGE = "out_of_range",
|
|
94
|
-
FORCED = "forced",
|
|
95
|
-
INITIAL = "initial",
|
|
96
|
-
STICKY = "sticky",
|
|
97
|
-
RULE = "rule",
|
|
98
|
-
ALLOCATED = "allocated",
|
|
99
|
-
DEFAULTED = "defaulted",
|
|
100
|
-
OVERRIDE = "override",
|
|
101
|
-
ERROR = "error",
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export interface Evaluation {
|
|
105
|
-
// required
|
|
106
|
-
featureKey: FeatureKey;
|
|
107
|
-
reason: EvaluationReason;
|
|
108
|
-
|
|
109
|
-
// common
|
|
110
|
-
bucketKey?: BucketKey;
|
|
111
|
-
bucketValue?: BucketValue;
|
|
112
|
-
ruleKey?: RuleKey;
|
|
113
|
-
error?: Error;
|
|
114
|
-
enabled?: boolean;
|
|
115
|
-
traffic?: Traffic;
|
|
116
|
-
forceIndex?: number;
|
|
117
|
-
force?: Force;
|
|
118
|
-
required?: Required[];
|
|
119
|
-
sticky?: OverrideFeature;
|
|
120
|
-
initial?: OverrideFeature;
|
|
121
|
-
|
|
122
|
-
// variation
|
|
123
|
-
variation?: Variation;
|
|
124
|
-
variationValue?: VariationValue;
|
|
125
|
-
|
|
126
|
-
// variable
|
|
127
|
-
variableKey?: VariableKey;
|
|
128
|
-
variableValue?: VariableValue;
|
|
129
|
-
variableSchema?: VariableSchema;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
66
|
function fetchDatafileContent(
|
|
133
67
|
datafileUrl,
|
|
134
68
|
handleDatafileFetch?: DatafileFetchHandler,
|
|
@@ -321,83 +255,6 @@ export class FeaturevisorInstance {
|
|
|
321
255
|
: featureKey; // full feature provided
|
|
322
256
|
}
|
|
323
257
|
|
|
324
|
-
/**
|
|
325
|
-
* Bucketing
|
|
326
|
-
*/
|
|
327
|
-
private getBucketKey(feature: Feature, context: Context): BucketKey {
|
|
328
|
-
const featureKey = feature.key;
|
|
329
|
-
|
|
330
|
-
let type;
|
|
331
|
-
let attributeKeys;
|
|
332
|
-
|
|
333
|
-
if (typeof feature.bucketBy === "string") {
|
|
334
|
-
type = "plain";
|
|
335
|
-
attributeKeys = [feature.bucketBy];
|
|
336
|
-
} else if (Array.isArray(feature.bucketBy)) {
|
|
337
|
-
type = "and";
|
|
338
|
-
attributeKeys = feature.bucketBy;
|
|
339
|
-
} else if (typeof feature.bucketBy === "object" && Array.isArray(feature.bucketBy.or)) {
|
|
340
|
-
type = "or";
|
|
341
|
-
attributeKeys = feature.bucketBy.or;
|
|
342
|
-
} else {
|
|
343
|
-
this.logger.error("invalid bucketBy", { featureKey, bucketBy: feature.bucketBy });
|
|
344
|
-
|
|
345
|
-
throw new Error("invalid bucketBy");
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const bucketKey: AttributeValue[] = [];
|
|
349
|
-
|
|
350
|
-
attributeKeys.forEach((attributeKey) => {
|
|
351
|
-
const attributeValue = context[attributeKey];
|
|
352
|
-
|
|
353
|
-
if (typeof attributeValue === "undefined") {
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (type === "plain" || type === "and") {
|
|
358
|
-
bucketKey.push(attributeValue);
|
|
359
|
-
} else {
|
|
360
|
-
// or
|
|
361
|
-
if (bucketKey.length === 0) {
|
|
362
|
-
bucketKey.push(attributeValue);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
bucketKey.push(featureKey);
|
|
368
|
-
|
|
369
|
-
const result = bucketKey.join(this.bucketKeySeparator);
|
|
370
|
-
|
|
371
|
-
if (this.configureBucketKey) {
|
|
372
|
-
return this.configureBucketKey(feature, context, result);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
return result;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
private getBucketValue(
|
|
379
|
-
feature: Feature,
|
|
380
|
-
context: Context,
|
|
381
|
-
): { bucketKey: BucketKey; bucketValue: BucketValue } {
|
|
382
|
-
const bucketKey = this.getBucketKey(feature, context);
|
|
383
|
-
|
|
384
|
-
const value = getBucketedNumber(bucketKey);
|
|
385
|
-
|
|
386
|
-
if (this.configureBucketValue) {
|
|
387
|
-
const configuredValue = this.configureBucketValue(feature, context, value);
|
|
388
|
-
|
|
389
|
-
return {
|
|
390
|
-
bucketKey,
|
|
391
|
-
bucketValue: configuredValue,
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return {
|
|
396
|
-
bucketKey,
|
|
397
|
-
bucketValue: value,
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
|
|
401
258
|
/**
|
|
402
259
|
* Statuses
|
|
403
260
|
*/
|
|
@@ -474,241 +331,24 @@ export class FeaturevisorInstance {
|
|
|
474
331
|
* Flag
|
|
475
332
|
*/
|
|
476
333
|
evaluateFlag(featureKey: FeatureKey | Feature, context: Context = {}): Evaluation {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
try {
|
|
480
|
-
const key = typeof featureKey === "string" ? featureKey : featureKey.key;
|
|
481
|
-
|
|
482
|
-
// sticky
|
|
483
|
-
if (
|
|
484
|
-
this.stickyFeatures &&
|
|
485
|
-
this.stickyFeatures[key] &&
|
|
486
|
-
typeof this.stickyFeatures[key].enabled !== "undefined"
|
|
487
|
-
) {
|
|
488
|
-
evaluation = {
|
|
489
|
-
featureKey: key,
|
|
490
|
-
reason: EvaluationReason.STICKY,
|
|
491
|
-
sticky: this.stickyFeatures[key],
|
|
492
|
-
enabled: this.stickyFeatures[key].enabled,
|
|
493
|
-
};
|
|
494
|
-
|
|
495
|
-
this.logger.debug("using sticky enabled", evaluation);
|
|
496
|
-
|
|
497
|
-
return evaluation;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// initial
|
|
501
|
-
if (
|
|
502
|
-
this.statuses &&
|
|
503
|
-
!this.statuses.ready &&
|
|
504
|
-
this.initialFeatures &&
|
|
505
|
-
this.initialFeatures[key] &&
|
|
506
|
-
typeof this.initialFeatures[key].enabled !== "undefined"
|
|
507
|
-
) {
|
|
508
|
-
evaluation = {
|
|
509
|
-
featureKey: key,
|
|
510
|
-
reason: EvaluationReason.INITIAL,
|
|
511
|
-
initial: this.initialFeatures[key],
|
|
512
|
-
enabled: this.initialFeatures[key].enabled,
|
|
513
|
-
};
|
|
514
|
-
|
|
515
|
-
this.logger.debug("using initial enabled", evaluation);
|
|
516
|
-
|
|
517
|
-
return evaluation;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const feature = this.getFeature(featureKey);
|
|
521
|
-
|
|
522
|
-
// not found
|
|
523
|
-
if (!feature) {
|
|
524
|
-
evaluation = {
|
|
525
|
-
featureKey: key,
|
|
526
|
-
reason: EvaluationReason.NOT_FOUND,
|
|
527
|
-
};
|
|
528
|
-
|
|
529
|
-
this.logger.warn("feature not found", evaluation);
|
|
530
|
-
|
|
531
|
-
return evaluation;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// deprecated
|
|
535
|
-
if (feature.deprecated) {
|
|
536
|
-
this.logger.warn("feature is deprecated", { featureKey: feature.key });
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
const finalContext = this.interceptContext ? this.interceptContext(context) : context;
|
|
540
|
-
|
|
541
|
-
// forced
|
|
542
|
-
const { force, forceIndex } = findForceFromFeature(
|
|
543
|
-
feature,
|
|
544
|
-
context,
|
|
545
|
-
this.datafileReader,
|
|
546
|
-
this.logger,
|
|
547
|
-
);
|
|
548
|
-
|
|
549
|
-
if (force && typeof force.enabled !== "undefined") {
|
|
550
|
-
evaluation = {
|
|
551
|
-
featureKey: feature.key,
|
|
552
|
-
reason: EvaluationReason.FORCED,
|
|
553
|
-
forceIndex,
|
|
554
|
-
force,
|
|
555
|
-
enabled: force.enabled,
|
|
556
|
-
};
|
|
557
|
-
|
|
558
|
-
this.logger.debug("forced enabled found", evaluation);
|
|
559
|
-
|
|
560
|
-
return evaluation;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// required
|
|
564
|
-
if (feature.required && feature.required.length > 0) {
|
|
565
|
-
const requiredFeaturesAreEnabled = feature.required.every((required) => {
|
|
566
|
-
let requiredKey;
|
|
567
|
-
let requiredVariation;
|
|
568
|
-
|
|
569
|
-
if (typeof required === "string") {
|
|
570
|
-
requiredKey = required;
|
|
571
|
-
} else {
|
|
572
|
-
requiredKey = required.key;
|
|
573
|
-
requiredVariation = required.variation;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
const requiredIsEnabled = this.isEnabled(requiredKey, finalContext);
|
|
577
|
-
|
|
578
|
-
if (!requiredIsEnabled) {
|
|
579
|
-
return false;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
if (typeof requiredVariation !== "undefined") {
|
|
583
|
-
const requiredVariationValue = this.getVariation(requiredKey, finalContext);
|
|
584
|
-
|
|
585
|
-
return requiredVariationValue === requiredVariation;
|
|
586
|
-
}
|
|
334
|
+
return evaluate({
|
|
335
|
+
type: "flag",
|
|
587
336
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
if (!requiredFeaturesAreEnabled) {
|
|
592
|
-
evaluation = {
|
|
593
|
-
featureKey: feature.key,
|
|
594
|
-
reason: EvaluationReason.REQUIRED,
|
|
595
|
-
required: feature.required,
|
|
596
|
-
enabled: requiredFeaturesAreEnabled,
|
|
597
|
-
};
|
|
337
|
+
featureKey,
|
|
338
|
+
context,
|
|
598
339
|
|
|
599
|
-
|
|
340
|
+
logger: this.logger,
|
|
341
|
+
datafileReader: this.datafileReader,
|
|
342
|
+
statuses: this.statuses,
|
|
343
|
+
interceptContext: this.interceptContext,
|
|
600
344
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
}
|
|
345
|
+
stickyFeatures: this.stickyFeatures,
|
|
346
|
+
initialFeatures: this.initialFeatures,
|
|
604
347
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
feature.traffic,
|
|
610
|
-
finalContext,
|
|
611
|
-
this.datafileReader,
|
|
612
|
-
this.logger,
|
|
613
|
-
);
|
|
614
|
-
|
|
615
|
-
if (matchedTraffic) {
|
|
616
|
-
// check if mutually exclusive
|
|
617
|
-
if (feature.ranges && feature.ranges.length > 0) {
|
|
618
|
-
const matchedRange = feature.ranges.find((range) => {
|
|
619
|
-
return bucketValue >= range[0] && bucketValue < range[1];
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
// matched
|
|
623
|
-
if (matchedRange) {
|
|
624
|
-
evaluation = {
|
|
625
|
-
featureKey: feature.key,
|
|
626
|
-
reason: EvaluationReason.ALLOCATED,
|
|
627
|
-
bucketKey,
|
|
628
|
-
bucketValue,
|
|
629
|
-
ruleKey: matchedTraffic.key,
|
|
630
|
-
traffic: matchedTraffic,
|
|
631
|
-
enabled:
|
|
632
|
-
typeof matchedTraffic.enabled === "undefined" ? true : matchedTraffic.enabled,
|
|
633
|
-
};
|
|
634
|
-
|
|
635
|
-
this.logger.debug("matched", evaluation);
|
|
636
|
-
|
|
637
|
-
return evaluation;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// no match
|
|
641
|
-
evaluation = {
|
|
642
|
-
featureKey: feature.key,
|
|
643
|
-
reason: EvaluationReason.OUT_OF_RANGE,
|
|
644
|
-
bucketKey,
|
|
645
|
-
bucketValue,
|
|
646
|
-
enabled: false,
|
|
647
|
-
};
|
|
648
|
-
|
|
649
|
-
this.logger.debug("not matched", evaluation);
|
|
650
|
-
|
|
651
|
-
return evaluation;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// override from rule
|
|
655
|
-
if (typeof matchedTraffic.enabled !== "undefined") {
|
|
656
|
-
evaluation = {
|
|
657
|
-
featureKey: feature.key,
|
|
658
|
-
reason: EvaluationReason.OVERRIDE,
|
|
659
|
-
bucketKey,
|
|
660
|
-
bucketValue,
|
|
661
|
-
ruleKey: matchedTraffic.key,
|
|
662
|
-
traffic: matchedTraffic,
|
|
663
|
-
enabled: matchedTraffic.enabled,
|
|
664
|
-
};
|
|
665
|
-
|
|
666
|
-
this.logger.debug("override from rule", evaluation);
|
|
667
|
-
|
|
668
|
-
return evaluation;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
// treated as enabled because of matched traffic
|
|
672
|
-
if (bucketValue <= matchedTraffic.percentage) {
|
|
673
|
-
evaluation = {
|
|
674
|
-
featureKey: feature.key,
|
|
675
|
-
reason: EvaluationReason.RULE,
|
|
676
|
-
bucketKey,
|
|
677
|
-
bucketValue,
|
|
678
|
-
ruleKey: matchedTraffic.key,
|
|
679
|
-
traffic: matchedTraffic,
|
|
680
|
-
enabled: true,
|
|
681
|
-
};
|
|
682
|
-
|
|
683
|
-
this.logger.debug("matched traffic", evaluation);
|
|
684
|
-
|
|
685
|
-
return evaluation;
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// nothing matched
|
|
690
|
-
evaluation = {
|
|
691
|
-
featureKey: feature.key,
|
|
692
|
-
reason: EvaluationReason.NO_MATCH,
|
|
693
|
-
bucketKey,
|
|
694
|
-
bucketValue,
|
|
695
|
-
enabled: false,
|
|
696
|
-
};
|
|
697
|
-
|
|
698
|
-
this.logger.debug("nothing matched", evaluation);
|
|
699
|
-
|
|
700
|
-
return evaluation;
|
|
701
|
-
} catch (e) {
|
|
702
|
-
evaluation = {
|
|
703
|
-
featureKey: typeof featureKey === "string" ? featureKey : featureKey.key,
|
|
704
|
-
reason: EvaluationReason.ERROR,
|
|
705
|
-
error: e,
|
|
706
|
-
};
|
|
707
|
-
|
|
708
|
-
this.logger.error("error", evaluation);
|
|
709
|
-
|
|
710
|
-
return evaluation;
|
|
711
|
-
}
|
|
348
|
+
bucketKeySeparator: this.bucketKeySeparator,
|
|
349
|
+
configureBucketKey: this.configureBucketKey,
|
|
350
|
+
configureBucketValue: this.configureBucketValue,
|
|
351
|
+
});
|
|
712
352
|
}
|
|
713
353
|
|
|
714
354
|
isEnabled(featureKey: FeatureKey | Feature, context: Context = {}): boolean {
|
|
@@ -727,193 +367,24 @@ export class FeaturevisorInstance {
|
|
|
727
367
|
* Variation
|
|
728
368
|
*/
|
|
729
369
|
evaluateVariation(featureKey: FeatureKey | Feature, context: Context = {}): Evaluation {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
try {
|
|
733
|
-
const key = typeof featureKey === "string" ? featureKey : featureKey.key;
|
|
734
|
-
|
|
735
|
-
const flag = this.evaluateFlag(featureKey, context);
|
|
736
|
-
|
|
737
|
-
if (flag.enabled === false) {
|
|
738
|
-
evaluation = {
|
|
739
|
-
featureKey: key,
|
|
740
|
-
reason: EvaluationReason.DISABLED,
|
|
741
|
-
};
|
|
742
|
-
|
|
743
|
-
this.logger.debug("feature is disabled", evaluation);
|
|
744
|
-
|
|
745
|
-
return evaluation;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
// sticky
|
|
749
|
-
if (this.stickyFeatures && this.stickyFeatures[key]) {
|
|
750
|
-
const variationValue = this.stickyFeatures[key].variation;
|
|
751
|
-
|
|
752
|
-
if (typeof variationValue !== "undefined") {
|
|
753
|
-
evaluation = {
|
|
754
|
-
featureKey: key,
|
|
755
|
-
reason: EvaluationReason.STICKY,
|
|
756
|
-
variationValue,
|
|
757
|
-
};
|
|
758
|
-
|
|
759
|
-
this.logger.debug("using sticky variation", evaluation);
|
|
370
|
+
return evaluate({
|
|
371
|
+
type: "variation",
|
|
760
372
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
// initial
|
|
766
|
-
if (
|
|
767
|
-
this.statuses &&
|
|
768
|
-
!this.statuses.ready &&
|
|
769
|
-
this.initialFeatures &&
|
|
770
|
-
this.initialFeatures[key] &&
|
|
771
|
-
typeof this.initialFeatures[key].variation !== "undefined"
|
|
772
|
-
) {
|
|
773
|
-
const variationValue = this.initialFeatures[key].variation;
|
|
774
|
-
|
|
775
|
-
evaluation = {
|
|
776
|
-
featureKey: key,
|
|
777
|
-
reason: EvaluationReason.INITIAL,
|
|
778
|
-
variationValue,
|
|
779
|
-
};
|
|
780
|
-
|
|
781
|
-
this.logger.debug("using initial variation", evaluation);
|
|
782
|
-
|
|
783
|
-
return evaluation;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
const feature = this.getFeature(featureKey);
|
|
787
|
-
|
|
788
|
-
// not found
|
|
789
|
-
if (!feature) {
|
|
790
|
-
evaluation = {
|
|
791
|
-
featureKey: key,
|
|
792
|
-
reason: EvaluationReason.NOT_FOUND,
|
|
793
|
-
};
|
|
794
|
-
|
|
795
|
-
this.logger.warn("feature not found", evaluation);
|
|
796
|
-
|
|
797
|
-
return evaluation;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// no variations
|
|
801
|
-
if (!feature.variations || feature.variations.length === 0) {
|
|
802
|
-
evaluation = {
|
|
803
|
-
featureKey: key,
|
|
804
|
-
reason: EvaluationReason.NO_VARIATIONS,
|
|
805
|
-
};
|
|
806
|
-
|
|
807
|
-
this.logger.warn("no variations", evaluation);
|
|
808
|
-
|
|
809
|
-
return evaluation;
|
|
810
|
-
}
|
|
373
|
+
featureKey,
|
|
374
|
+
context,
|
|
811
375
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
feature,
|
|
817
|
-
context,
|
|
818
|
-
this.datafileReader,
|
|
819
|
-
this.logger,
|
|
820
|
-
);
|
|
821
|
-
|
|
822
|
-
if (force && force.variation) {
|
|
823
|
-
const variation = feature.variations.find((v) => v.value === force.variation);
|
|
376
|
+
logger: this.logger,
|
|
377
|
+
datafileReader: this.datafileReader,
|
|
378
|
+
statuses: this.statuses,
|
|
379
|
+
interceptContext: this.interceptContext,
|
|
824
380
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
featureKey: feature.key,
|
|
828
|
-
reason: EvaluationReason.FORCED,
|
|
829
|
-
forceIndex,
|
|
830
|
-
force,
|
|
831
|
-
variation,
|
|
832
|
-
};
|
|
381
|
+
stickyFeatures: this.stickyFeatures,
|
|
382
|
+
initialFeatures: this.initialFeatures,
|
|
833
383
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
// bucketing
|
|
841
|
-
const { bucketKey, bucketValue } = this.getBucketValue(feature, finalContext);
|
|
842
|
-
|
|
843
|
-
const { matchedTraffic, matchedAllocation } = getMatchedTrafficAndAllocation(
|
|
844
|
-
feature.traffic,
|
|
845
|
-
finalContext,
|
|
846
|
-
bucketValue,
|
|
847
|
-
this.datafileReader,
|
|
848
|
-
this.logger,
|
|
849
|
-
);
|
|
850
|
-
|
|
851
|
-
if (matchedTraffic) {
|
|
852
|
-
// override from rule
|
|
853
|
-
if (matchedTraffic.variation) {
|
|
854
|
-
const variation = feature.variations.find((v) => v.value === matchedTraffic.variation);
|
|
855
|
-
|
|
856
|
-
if (variation) {
|
|
857
|
-
evaluation = {
|
|
858
|
-
featureKey: feature.key,
|
|
859
|
-
reason: EvaluationReason.RULE,
|
|
860
|
-
bucketKey,
|
|
861
|
-
bucketValue,
|
|
862
|
-
ruleKey: matchedTraffic.key,
|
|
863
|
-
traffic: matchedTraffic,
|
|
864
|
-
variation,
|
|
865
|
-
};
|
|
866
|
-
|
|
867
|
-
this.logger.debug("override from rule", evaluation);
|
|
868
|
-
|
|
869
|
-
return evaluation;
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
// regular allocation
|
|
874
|
-
if (matchedAllocation && matchedAllocation.variation) {
|
|
875
|
-
const variation = feature.variations.find((v) => v.value === matchedAllocation.variation);
|
|
876
|
-
|
|
877
|
-
if (variation) {
|
|
878
|
-
evaluation = {
|
|
879
|
-
featureKey: feature.key,
|
|
880
|
-
reason: EvaluationReason.ALLOCATED,
|
|
881
|
-
bucketKey,
|
|
882
|
-
bucketValue,
|
|
883
|
-
ruleKey: matchedTraffic.key,
|
|
884
|
-
traffic: matchedTraffic,
|
|
885
|
-
variation,
|
|
886
|
-
};
|
|
887
|
-
|
|
888
|
-
this.logger.debug("allocated variation", evaluation);
|
|
889
|
-
|
|
890
|
-
return evaluation;
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// nothing matched
|
|
896
|
-
evaluation = {
|
|
897
|
-
featureKey: feature.key,
|
|
898
|
-
reason: EvaluationReason.NO_MATCH,
|
|
899
|
-
bucketKey,
|
|
900
|
-
bucketValue,
|
|
901
|
-
};
|
|
902
|
-
|
|
903
|
-
this.logger.debug("no matched variation", evaluation);
|
|
904
|
-
|
|
905
|
-
return evaluation;
|
|
906
|
-
} catch (e) {
|
|
907
|
-
evaluation = {
|
|
908
|
-
featureKey: typeof featureKey === "string" ? featureKey : featureKey.key,
|
|
909
|
-
reason: EvaluationReason.ERROR,
|
|
910
|
-
error: e,
|
|
911
|
-
};
|
|
912
|
-
|
|
913
|
-
this.logger.error("error", evaluation);
|
|
914
|
-
|
|
915
|
-
return evaluation;
|
|
916
|
-
}
|
|
384
|
+
bucketKeySeparator: this.bucketKeySeparator,
|
|
385
|
+
configureBucketKey: this.configureBucketKey,
|
|
386
|
+
configureBucketValue: this.configureBucketValue,
|
|
387
|
+
});
|
|
917
388
|
}
|
|
918
389
|
|
|
919
390
|
getVariation(
|
|
@@ -992,268 +463,25 @@ export class FeaturevisorInstance {
|
|
|
992
463
|
variableKey: VariableKey,
|
|
993
464
|
context: Context = {},
|
|
994
465
|
): Evaluation {
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
try {
|
|
998
|
-
const key = typeof featureKey === "string" ? featureKey : featureKey.key;
|
|
999
|
-
|
|
1000
|
-
const flag = this.evaluateFlag(featureKey, context);
|
|
1001
|
-
|
|
1002
|
-
if (flag.enabled === false) {
|
|
1003
|
-
evaluation = {
|
|
1004
|
-
featureKey: key,
|
|
1005
|
-
reason: EvaluationReason.DISABLED,
|
|
1006
|
-
};
|
|
1007
|
-
|
|
1008
|
-
this.logger.debug("feature is disabled", evaluation);
|
|
1009
|
-
|
|
1010
|
-
return evaluation;
|
|
1011
|
-
}
|
|
466
|
+
return evaluate({
|
|
467
|
+
type: "variable",
|
|
1012
468
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
469
|
+
featureKey,
|
|
470
|
+
variableKey,
|
|
471
|
+
context,
|
|
1016
472
|
|
|
1017
|
-
|
|
1018
|
-
|
|
473
|
+
logger: this.logger,
|
|
474
|
+
datafileReader: this.datafileReader,
|
|
475
|
+
statuses: this.statuses,
|
|
476
|
+
interceptContext: this.interceptContext,
|
|
1019
477
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
featureKey: key,
|
|
1023
|
-
reason: EvaluationReason.STICKY,
|
|
1024
|
-
variableKey,
|
|
1025
|
-
variableValue: result,
|
|
1026
|
-
};
|
|
478
|
+
stickyFeatures: this.stickyFeatures,
|
|
479
|
+
initialFeatures: this.initialFeatures,
|
|
1027
480
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
// initial
|
|
1036
|
-
if (
|
|
1037
|
-
this.statuses &&
|
|
1038
|
-
!this.statuses.ready &&
|
|
1039
|
-
this.initialFeatures &&
|
|
1040
|
-
this.initialFeatures[key]
|
|
1041
|
-
) {
|
|
1042
|
-
const variables = this.initialFeatures[key].variables;
|
|
1043
|
-
|
|
1044
|
-
if (variables) {
|
|
1045
|
-
if (typeof variables[variableKey] !== "undefined") {
|
|
1046
|
-
evaluation = {
|
|
1047
|
-
featureKey: key,
|
|
1048
|
-
reason: EvaluationReason.INITIAL,
|
|
1049
|
-
variableKey,
|
|
1050
|
-
variableValue: variables[variableKey],
|
|
1051
|
-
};
|
|
1052
|
-
|
|
1053
|
-
this.logger.debug("using initial variable", evaluation);
|
|
1054
|
-
|
|
1055
|
-
return evaluation;
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
const feature = this.getFeature(featureKey);
|
|
1061
|
-
|
|
1062
|
-
// not found
|
|
1063
|
-
if (!feature) {
|
|
1064
|
-
evaluation = {
|
|
1065
|
-
featureKey: key,
|
|
1066
|
-
reason: EvaluationReason.NOT_FOUND,
|
|
1067
|
-
variableKey,
|
|
1068
|
-
};
|
|
1069
|
-
|
|
1070
|
-
this.logger.warn("feature not found in datafile", evaluation);
|
|
1071
|
-
|
|
1072
|
-
return evaluation;
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
const variableSchema = Array.isArray(feature.variablesSchema)
|
|
1076
|
-
? feature.variablesSchema.find((v) => v.key === variableKey)
|
|
1077
|
-
: undefined;
|
|
1078
|
-
|
|
1079
|
-
// variable schema not found
|
|
1080
|
-
if (!variableSchema) {
|
|
1081
|
-
evaluation = {
|
|
1082
|
-
featureKey: key,
|
|
1083
|
-
reason: EvaluationReason.NOT_FOUND,
|
|
1084
|
-
variableKey,
|
|
1085
|
-
};
|
|
1086
|
-
|
|
1087
|
-
this.logger.warn("variable schema not found", evaluation);
|
|
1088
|
-
|
|
1089
|
-
return evaluation;
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
const finalContext = this.interceptContext ? this.interceptContext(context) : context;
|
|
1093
|
-
|
|
1094
|
-
// forced
|
|
1095
|
-
const { force, forceIndex } = findForceFromFeature(
|
|
1096
|
-
feature,
|
|
1097
|
-
context,
|
|
1098
|
-
this.datafileReader,
|
|
1099
|
-
this.logger,
|
|
1100
|
-
);
|
|
1101
|
-
|
|
1102
|
-
if (force && force.variables && typeof force.variables[variableKey] !== "undefined") {
|
|
1103
|
-
evaluation = {
|
|
1104
|
-
featureKey: feature.key,
|
|
1105
|
-
reason: EvaluationReason.FORCED,
|
|
1106
|
-
forceIndex,
|
|
1107
|
-
force,
|
|
1108
|
-
variableKey,
|
|
1109
|
-
variableSchema,
|
|
1110
|
-
variableValue: force.variables[variableKey],
|
|
1111
|
-
};
|
|
1112
|
-
|
|
1113
|
-
this.logger.debug("forced variable", evaluation);
|
|
1114
|
-
|
|
1115
|
-
return evaluation;
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
// bucketing
|
|
1119
|
-
const { bucketKey, bucketValue } = this.getBucketValue(feature, finalContext);
|
|
1120
|
-
|
|
1121
|
-
const { matchedTraffic, matchedAllocation } = getMatchedTrafficAndAllocation(
|
|
1122
|
-
feature.traffic,
|
|
1123
|
-
finalContext,
|
|
1124
|
-
bucketValue,
|
|
1125
|
-
this.datafileReader,
|
|
1126
|
-
this.logger,
|
|
1127
|
-
);
|
|
1128
|
-
|
|
1129
|
-
if (matchedTraffic) {
|
|
1130
|
-
// override from rule
|
|
1131
|
-
if (
|
|
1132
|
-
matchedTraffic.variables &&
|
|
1133
|
-
typeof matchedTraffic.variables[variableKey] !== "undefined"
|
|
1134
|
-
) {
|
|
1135
|
-
evaluation = {
|
|
1136
|
-
featureKey: feature.key,
|
|
1137
|
-
reason: EvaluationReason.RULE,
|
|
1138
|
-
bucketKey,
|
|
1139
|
-
bucketValue,
|
|
1140
|
-
ruleKey: matchedTraffic.key,
|
|
1141
|
-
traffic: matchedTraffic,
|
|
1142
|
-
variableKey,
|
|
1143
|
-
variableSchema,
|
|
1144
|
-
variableValue: matchedTraffic.variables[variableKey],
|
|
1145
|
-
};
|
|
1146
|
-
|
|
1147
|
-
this.logger.debug("override from rule", evaluation);
|
|
1148
|
-
|
|
1149
|
-
return evaluation;
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
// regular allocation
|
|
1153
|
-
let variationValue;
|
|
1154
|
-
|
|
1155
|
-
if (force && force.variation) {
|
|
1156
|
-
variationValue = force.variation;
|
|
1157
|
-
} else if (matchedAllocation && matchedAllocation.variation) {
|
|
1158
|
-
variationValue = matchedAllocation.variation;
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
if (variationValue && Array.isArray(feature.variations)) {
|
|
1162
|
-
const variation = feature.variations.find((v) => v.value === variationValue);
|
|
1163
|
-
|
|
1164
|
-
if (variation && variation.variables) {
|
|
1165
|
-
const variableFromVariation = variation.variables.find((v) => v.key === variableKey);
|
|
1166
|
-
|
|
1167
|
-
if (variableFromVariation) {
|
|
1168
|
-
if (variableFromVariation.overrides) {
|
|
1169
|
-
const override = variableFromVariation.overrides.find((o) => {
|
|
1170
|
-
if (o.conditions) {
|
|
1171
|
-
return allConditionsAreMatched(
|
|
1172
|
-
typeof o.conditions === "string" ? JSON.parse(o.conditions) : o.conditions,
|
|
1173
|
-
finalContext,
|
|
1174
|
-
this.logger,
|
|
1175
|
-
);
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
if (o.segments) {
|
|
1179
|
-
return allGroupSegmentsAreMatched(
|
|
1180
|
-
parseFromStringifiedSegments(o.segments),
|
|
1181
|
-
finalContext,
|
|
1182
|
-
this.datafileReader,
|
|
1183
|
-
this.logger,
|
|
1184
|
-
);
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
return false;
|
|
1188
|
-
});
|
|
1189
|
-
|
|
1190
|
-
if (override) {
|
|
1191
|
-
evaluation = {
|
|
1192
|
-
featureKey: feature.key,
|
|
1193
|
-
reason: EvaluationReason.OVERRIDE,
|
|
1194
|
-
bucketKey,
|
|
1195
|
-
bucketValue,
|
|
1196
|
-
ruleKey: matchedTraffic.key,
|
|
1197
|
-
traffic: matchedTraffic,
|
|
1198
|
-
variableKey,
|
|
1199
|
-
variableSchema,
|
|
1200
|
-
variableValue: override.value,
|
|
1201
|
-
};
|
|
1202
|
-
|
|
1203
|
-
this.logger.debug("variable override", evaluation);
|
|
1204
|
-
|
|
1205
|
-
return evaluation;
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
if (typeof variableFromVariation.value !== "undefined") {
|
|
1210
|
-
evaluation = {
|
|
1211
|
-
featureKey: feature.key,
|
|
1212
|
-
reason: EvaluationReason.ALLOCATED,
|
|
1213
|
-
bucketKey,
|
|
1214
|
-
bucketValue,
|
|
1215
|
-
ruleKey: matchedTraffic.key,
|
|
1216
|
-
traffic: matchedTraffic,
|
|
1217
|
-
variableKey,
|
|
1218
|
-
variableSchema,
|
|
1219
|
-
variableValue: variableFromVariation.value,
|
|
1220
|
-
};
|
|
1221
|
-
|
|
1222
|
-
this.logger.debug("allocated variable", evaluation);
|
|
1223
|
-
|
|
1224
|
-
return evaluation;
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
// fall back to default
|
|
1232
|
-
evaluation = {
|
|
1233
|
-
featureKey: feature.key,
|
|
1234
|
-
reason: EvaluationReason.DEFAULTED,
|
|
1235
|
-
bucketKey,
|
|
1236
|
-
bucketValue,
|
|
1237
|
-
variableKey,
|
|
1238
|
-
variableSchema,
|
|
1239
|
-
variableValue: variableSchema.defaultValue,
|
|
1240
|
-
};
|
|
1241
|
-
|
|
1242
|
-
this.logger.debug("using default value", evaluation);
|
|
1243
|
-
|
|
1244
|
-
return evaluation;
|
|
1245
|
-
} catch (e) {
|
|
1246
|
-
evaluation = {
|
|
1247
|
-
featureKey: typeof featureKey === "string" ? featureKey : featureKey.key,
|
|
1248
|
-
reason: EvaluationReason.ERROR,
|
|
1249
|
-
variableKey,
|
|
1250
|
-
error: e,
|
|
1251
|
-
};
|
|
1252
|
-
|
|
1253
|
-
this.logger.error("error", evaluation);
|
|
1254
|
-
|
|
1255
|
-
return evaluation;
|
|
1256
|
-
}
|
|
481
|
+
bucketKeySeparator: this.bucketKeySeparator,
|
|
482
|
+
configureBucketKey: this.configureBucketKey,
|
|
483
|
+
configureBucketValue: this.configureBucketValue,
|
|
484
|
+
});
|
|
1257
485
|
}
|
|
1258
486
|
|
|
1259
487
|
getVariable(
|