@featurevisor/sdk 0.37.0 → 0.38.0
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 +375 -312
- package/coverage/coverage-final.json +6 -6
- package/coverage/lcov-report/bucket.ts.html +4 -4
- package/coverage/lcov-report/conditions.ts.html +1 -1
- package/coverage/lcov-report/datafileReader.ts.html +4 -4
- package/coverage/lcov-report/emitter.ts.html +1 -1
- package/coverage/lcov-report/feature.ts.html +74 -14
- package/coverage/lcov-report/index.html +28 -28
- package/coverage/lcov-report/instance.ts.html +713 -140
- package/coverage/lcov-report/logger.ts.html +3 -3
- package/coverage/lcov-report/segments.ts.html +3 -3
- package/coverage/lcov.info +662 -560
- package/dist/index.js +1 -1
- package/dist/index.js.gz +0 -0
- package/dist/index.js.map +1 -1
- package/lib/feature.d.ts +1 -0
- package/lib/feature.js +8 -0
- package/lib/feature.js.map +1 -1
- package/lib/instance.d.ts +14 -6
- package/lib/instance.js +224 -77
- package/lib/instance.js.map +1 -1
- package/package.json +3 -3
- package/src/feature.ts +20 -0
- package/src/instance.spec.ts +58 -55
- package/src/instance.ts +263 -72
package/src/instance.ts
CHANGED
|
@@ -7,10 +7,11 @@ import {
|
|
|
7
7
|
Feature,
|
|
8
8
|
FeatureKey,
|
|
9
9
|
InitialFeatures,
|
|
10
|
+
OverrideFeature,
|
|
10
11
|
StickyFeatures,
|
|
12
|
+
Traffic,
|
|
11
13
|
VariableType,
|
|
12
14
|
VariableValue,
|
|
13
|
-
VariationType,
|
|
14
15
|
VariationValue,
|
|
15
16
|
Variation,
|
|
16
17
|
RuleKey,
|
|
@@ -22,7 +23,7 @@ import { createLogger, Logger } from "./logger";
|
|
|
22
23
|
import { DatafileReader } from "./datafileReader";
|
|
23
24
|
import { Emitter } from "./emitter";
|
|
24
25
|
import { getBucketedNumber } from "./bucket";
|
|
25
|
-
import { findForceFromFeature, getMatchedTrafficAndAllocation } from "./feature";
|
|
26
|
+
import { findForceFromFeature, getMatchedTraffic, getMatchedTrafficAndAllocation } from "./feature";
|
|
26
27
|
import { allConditionsAreMatched } from "./conditions";
|
|
27
28
|
import { allGroupSegmentsAreMatched } from "./segments";
|
|
28
29
|
|
|
@@ -78,6 +79,9 @@ export type DatafileFetchHandler = (datafileUrl: string) => Promise<DatafileCont
|
|
|
78
79
|
|
|
79
80
|
export enum EvaluationReason {
|
|
80
81
|
NOT_FOUND = "not_found",
|
|
82
|
+
NO_VARIATIONS = "no_variations",
|
|
83
|
+
DISABLED = "disabled",
|
|
84
|
+
OUT_OF_RANGE = "out_of_range",
|
|
81
85
|
FORCED = "forced",
|
|
82
86
|
INITIAL = "initial",
|
|
83
87
|
STICKY = "sticky",
|
|
@@ -97,6 +101,10 @@ export interface Evaluation {
|
|
|
97
101
|
bucketValue?: BucketValue;
|
|
98
102
|
ruleKey?: RuleKey;
|
|
99
103
|
error?: Error;
|
|
104
|
+
enabled?: boolean;
|
|
105
|
+
traffic?: Traffic;
|
|
106
|
+
sticky?: OverrideFeature;
|
|
107
|
+
initial?: OverrideFeature;
|
|
100
108
|
|
|
101
109
|
// variation
|
|
102
110
|
variation?: Variation;
|
|
@@ -119,7 +127,7 @@ function fetchDatafileContent(
|
|
|
119
127
|
return fetch(datafileUrl).then((res) => res.json());
|
|
120
128
|
}
|
|
121
129
|
|
|
122
|
-
type FieldType =
|
|
130
|
+
type FieldType = string | VariableType;
|
|
123
131
|
type ValueType = VariableValue;
|
|
124
132
|
|
|
125
133
|
export function getValueByType(value: ValueType, fieldType: FieldType): ValueType {
|
|
@@ -418,6 +426,186 @@ export class FeaturevisorInstance {
|
|
|
418
426
|
clearInterval(this.intervalId);
|
|
419
427
|
}
|
|
420
428
|
|
|
429
|
+
/**
|
|
430
|
+
* Flag
|
|
431
|
+
*/
|
|
432
|
+
evaluateFlag(featureKey: FeatureKey | Feature, context: Context = {}): Evaluation {
|
|
433
|
+
let evaluation: Evaluation;
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
const key = typeof featureKey === "string" ? featureKey : featureKey.key;
|
|
437
|
+
|
|
438
|
+
// sticky
|
|
439
|
+
if (
|
|
440
|
+
this.stickyFeatures &&
|
|
441
|
+
this.stickyFeatures[key] &&
|
|
442
|
+
typeof this.stickyFeatures[key].enabled !== "undefined"
|
|
443
|
+
) {
|
|
444
|
+
evaluation = {
|
|
445
|
+
featureKey: key,
|
|
446
|
+
reason: EvaluationReason.STICKY,
|
|
447
|
+
enabled: this.stickyFeatures[key].enabled,
|
|
448
|
+
sticky: this.stickyFeatures[key],
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
this.logger.debug("using sticky enabled", evaluation);
|
|
452
|
+
|
|
453
|
+
return evaluation;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// initial
|
|
457
|
+
if (
|
|
458
|
+
this.statuses &&
|
|
459
|
+
!this.statuses.ready &&
|
|
460
|
+
this.initialFeatures &&
|
|
461
|
+
this.initialFeatures[key] &&
|
|
462
|
+
typeof this.initialFeatures[key].enabled !== "undefined"
|
|
463
|
+
) {
|
|
464
|
+
evaluation = {
|
|
465
|
+
featureKey: key,
|
|
466
|
+
reason: EvaluationReason.INITIAL,
|
|
467
|
+
enabled: this.initialFeatures[key].enabled,
|
|
468
|
+
initial: this.initialFeatures[key],
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
this.logger.debug("using initial enabled", evaluation);
|
|
472
|
+
|
|
473
|
+
return evaluation;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const feature = this.getFeature(featureKey);
|
|
477
|
+
|
|
478
|
+
// not found
|
|
479
|
+
if (!feature) {
|
|
480
|
+
evaluation = {
|
|
481
|
+
featureKey: key,
|
|
482
|
+
reason: EvaluationReason.NOT_FOUND,
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
this.logger.warn("feature not found", evaluation);
|
|
486
|
+
|
|
487
|
+
return evaluation;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const finalContext = this.interceptContext ? this.interceptContext(context) : context;
|
|
491
|
+
|
|
492
|
+
// forced
|
|
493
|
+
const force = findForceFromFeature(feature, context, this.datafileReader);
|
|
494
|
+
|
|
495
|
+
if (force && typeof force.enabled !== "undefined") {
|
|
496
|
+
evaluation = {
|
|
497
|
+
featureKey: feature.key,
|
|
498
|
+
reason: EvaluationReason.FORCED,
|
|
499
|
+
enabled: force.enabled,
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
this.logger.debug("forced enabled found", evaluation);
|
|
503
|
+
|
|
504
|
+
return evaluation;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// bucketing
|
|
508
|
+
const bucketValue = this.getBucketValue(feature, finalContext);
|
|
509
|
+
|
|
510
|
+
const matchedTraffic = getMatchedTraffic(feature.traffic, finalContext, this.datafileReader);
|
|
511
|
+
|
|
512
|
+
if (matchedTraffic) {
|
|
513
|
+
// check if mutually exclusive
|
|
514
|
+
if (feature.ranges && feature.ranges.length > 0) {
|
|
515
|
+
const matchedRange = feature.ranges.find((range) => {
|
|
516
|
+
return bucketValue >= range[0] && bucketValue < range[1];
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// matched
|
|
520
|
+
if (matchedRange) {
|
|
521
|
+
evaluation = {
|
|
522
|
+
featureKey: feature.key,
|
|
523
|
+
reason: EvaluationReason.ALLOCATED,
|
|
524
|
+
enabled:
|
|
525
|
+
typeof matchedTraffic.enabled === "undefined" ? true : matchedTraffic.enabled,
|
|
526
|
+
bucketValue,
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
return evaluation;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// no match
|
|
533
|
+
evaluation = {
|
|
534
|
+
featureKey: feature.key,
|
|
535
|
+
reason: EvaluationReason.OUT_OF_RANGE,
|
|
536
|
+
enabled: false,
|
|
537
|
+
bucketValue,
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
this.logger.debug("not matched", evaluation);
|
|
541
|
+
|
|
542
|
+
return evaluation;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// override from rule
|
|
546
|
+
if (typeof matchedTraffic.enabled !== "undefined") {
|
|
547
|
+
evaluation = {
|
|
548
|
+
featureKey: feature.key,
|
|
549
|
+
reason: EvaluationReason.OVERRIDE,
|
|
550
|
+
enabled: matchedTraffic.enabled,
|
|
551
|
+
bucketValue,
|
|
552
|
+
ruleKey: matchedTraffic.key,
|
|
553
|
+
traffic: matchedTraffic,
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
this.logger.debug("override from rule", evaluation);
|
|
557
|
+
|
|
558
|
+
return evaluation;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// treated as enabled because of matched traffic
|
|
562
|
+
if (bucketValue < matchedTraffic.percentage) {
|
|
563
|
+
// @TODO: verify if range check should be inclusive or not
|
|
564
|
+
evaluation = {
|
|
565
|
+
featureKey: feature.key,
|
|
566
|
+
reason: EvaluationReason.RULE,
|
|
567
|
+
enabled: true,
|
|
568
|
+
bucketValue,
|
|
569
|
+
ruleKey: matchedTraffic.key,
|
|
570
|
+
traffic: matchedTraffic,
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
return evaluation;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// nothing matched
|
|
578
|
+
evaluation = {
|
|
579
|
+
featureKey: feature.key,
|
|
580
|
+
reason: EvaluationReason.ERROR,
|
|
581
|
+
enabled: false,
|
|
582
|
+
bucketValue,
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
return evaluation;
|
|
586
|
+
} catch (e) {
|
|
587
|
+
evaluation = {
|
|
588
|
+
featureKey: typeof featureKey === "string" ? featureKey : featureKey.key,
|
|
589
|
+
reason: EvaluationReason.ERROR,
|
|
590
|
+
error: e,
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
return evaluation;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
isEnabled(featureKey: FeatureKey | Feature, context: Context = {}): boolean {
|
|
598
|
+
try {
|
|
599
|
+
const evaluation = this.evaluateFlag(featureKey, context);
|
|
600
|
+
|
|
601
|
+
return evaluation.enabled === true;
|
|
602
|
+
} catch (e) {
|
|
603
|
+
this.logger.error("isEnabled", { featureKey, error: e });
|
|
604
|
+
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
421
609
|
/**
|
|
422
610
|
* Variation
|
|
423
611
|
*/
|
|
@@ -427,6 +615,19 @@ export class FeaturevisorInstance {
|
|
|
427
615
|
try {
|
|
428
616
|
const key = typeof featureKey === "string" ? featureKey : featureKey.key;
|
|
429
617
|
|
|
618
|
+
const flag = this.evaluateFlag(featureKey, context);
|
|
619
|
+
|
|
620
|
+
if (flag.enabled === false) {
|
|
621
|
+
evaluation = {
|
|
622
|
+
featureKey: key,
|
|
623
|
+
reason: EvaluationReason.DISABLED,
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
this.logger.debug("feature is disabled", evaluation);
|
|
627
|
+
|
|
628
|
+
return evaluation;
|
|
629
|
+
}
|
|
630
|
+
|
|
430
631
|
// sticky
|
|
431
632
|
if (this.stickyFeatures && this.stickyFeatures[key]) {
|
|
432
633
|
const variationValue = this.stickyFeatures[key].variation;
|
|
@@ -474,7 +675,19 @@ export class FeaturevisorInstance {
|
|
|
474
675
|
reason: EvaluationReason.NOT_FOUND,
|
|
475
676
|
};
|
|
476
677
|
|
|
477
|
-
this.logger.warn("feature not found
|
|
678
|
+
this.logger.warn("feature not found", evaluation);
|
|
679
|
+
|
|
680
|
+
return evaluation;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// no variations
|
|
684
|
+
if (!feature.variations || feature.variations.length === 0) {
|
|
685
|
+
evaluation = {
|
|
686
|
+
featureKey: key,
|
|
687
|
+
reason: EvaluationReason.NO_VARIATIONS,
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
this.logger.warn("no variations", evaluation);
|
|
478
691
|
|
|
479
692
|
return evaluation;
|
|
480
693
|
}
|
|
@@ -550,30 +763,14 @@ export class FeaturevisorInstance {
|
|
|
550
763
|
}
|
|
551
764
|
}
|
|
552
765
|
|
|
553
|
-
//
|
|
554
|
-
const variation = feature.variations.find((v) => v.value === feature.defaultVariation);
|
|
555
|
-
|
|
556
|
-
if (variation) {
|
|
557
|
-
evaluation = {
|
|
558
|
-
featureKey: feature.key,
|
|
559
|
-
reason: EvaluationReason.DEFAULTED,
|
|
560
|
-
bucketValue,
|
|
561
|
-
variation,
|
|
562
|
-
};
|
|
563
|
-
|
|
564
|
-
this.logger.debug("using default variation", evaluation);
|
|
565
|
-
|
|
566
|
-
return evaluation;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// nothing matched (this should never happen)
|
|
766
|
+
// nothing matched
|
|
570
767
|
evaluation = {
|
|
571
768
|
featureKey: feature.key,
|
|
572
769
|
reason: EvaluationReason.ERROR,
|
|
573
770
|
bucketValue,
|
|
574
771
|
};
|
|
575
772
|
|
|
576
|
-
this.logger.
|
|
773
|
+
this.logger.debug("no matched variation", evaluation);
|
|
577
774
|
|
|
578
775
|
return evaluation;
|
|
579
776
|
} catch (e) {
|
|
@@ -610,33 +807,6 @@ export class FeaturevisorInstance {
|
|
|
610
807
|
}
|
|
611
808
|
}
|
|
612
809
|
|
|
613
|
-
getVariationBoolean(
|
|
614
|
-
featureKey: FeatureKey | Feature,
|
|
615
|
-
context: Context = {},
|
|
616
|
-
): boolean | undefined {
|
|
617
|
-
const variationValue = this.getVariation(featureKey, context);
|
|
618
|
-
|
|
619
|
-
return getValueByType(variationValue, "boolean") as boolean | undefined;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
getVariationString(featureKey: FeatureKey | Feature, context: Context = {}): string | undefined {
|
|
623
|
-
const variationValue = this.getVariation(featureKey, context);
|
|
624
|
-
|
|
625
|
-
return getValueByType(variationValue, "string") as string | undefined;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
getVariationInteger(featureKey: FeatureKey | Feature, context: Context = {}): number | undefined {
|
|
629
|
-
const variationValue = this.getVariation(featureKey, context);
|
|
630
|
-
|
|
631
|
-
return getValueByType(variationValue, "integer") as number | undefined;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
getVariationDouble(featureKey: FeatureKey | Feature, context: Context = {}): number | undefined {
|
|
635
|
-
const variationValue = this.getVariation(featureKey, context);
|
|
636
|
-
|
|
637
|
-
return getValueByType(variationValue, "double") as number | undefined;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
810
|
/**
|
|
641
811
|
* Activate
|
|
642
812
|
*/
|
|
@@ -719,21 +889,38 @@ export class FeaturevisorInstance {
|
|
|
719
889
|
try {
|
|
720
890
|
const key = typeof featureKey === "string" ? featureKey : featureKey.key;
|
|
721
891
|
|
|
892
|
+
const flag = this.evaluateFlag(featureKey, context);
|
|
893
|
+
|
|
894
|
+
if (flag.enabled === false) {
|
|
895
|
+
evaluation = {
|
|
896
|
+
featureKey: key,
|
|
897
|
+
reason: EvaluationReason.DISABLED,
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
this.logger.debug("feature is disabled", evaluation);
|
|
901
|
+
|
|
902
|
+
return evaluation;
|
|
903
|
+
}
|
|
904
|
+
|
|
722
905
|
// sticky
|
|
723
|
-
if (this.stickyFeatures && this.stickyFeatures[key]
|
|
724
|
-
const
|
|
906
|
+
if (this.stickyFeatures && this.stickyFeatures[key]) {
|
|
907
|
+
const variables = this.stickyFeatures[key].variables;
|
|
725
908
|
|
|
726
|
-
if (
|
|
727
|
-
|
|
728
|
-
featureKey: key,
|
|
729
|
-
reason: EvaluationReason.STICKY,
|
|
730
|
-
variableKey,
|
|
731
|
-
variableValue: result,
|
|
732
|
-
};
|
|
909
|
+
if (variables) {
|
|
910
|
+
const result = variables[variableKey];
|
|
733
911
|
|
|
734
|
-
|
|
912
|
+
if (typeof result !== "undefined") {
|
|
913
|
+
evaluation = {
|
|
914
|
+
featureKey: key,
|
|
915
|
+
reason: EvaluationReason.STICKY,
|
|
916
|
+
variableKey,
|
|
917
|
+
variableValue: result,
|
|
918
|
+
};
|
|
735
919
|
|
|
736
|
-
|
|
920
|
+
this.logger.debug("using sticky variable", evaluation);
|
|
921
|
+
|
|
922
|
+
return evaluation;
|
|
923
|
+
}
|
|
737
924
|
}
|
|
738
925
|
}
|
|
739
926
|
|
|
@@ -742,20 +929,24 @@ export class FeaturevisorInstance {
|
|
|
742
929
|
this.statuses &&
|
|
743
930
|
!this.statuses.ready &&
|
|
744
931
|
this.initialFeatures &&
|
|
745
|
-
this.initialFeatures[key]
|
|
746
|
-
this.initialFeatures[key].variables &&
|
|
747
|
-
typeof this.initialFeatures[key].variables[variableKey] !== "undefined"
|
|
932
|
+
this.initialFeatures[key]
|
|
748
933
|
) {
|
|
749
|
-
|
|
750
|
-
featureKey: key,
|
|
751
|
-
reason: EvaluationReason.INITIAL,
|
|
752
|
-
variableKey,
|
|
753
|
-
variableValue: this.initialFeatures[key].variables[variableKey],
|
|
754
|
-
};
|
|
934
|
+
const variables = this.initialFeatures[key].variables;
|
|
755
935
|
|
|
756
|
-
|
|
936
|
+
if (variables) {
|
|
937
|
+
if (typeof variables[variableKey] !== "undefined") {
|
|
938
|
+
evaluation = {
|
|
939
|
+
featureKey: key,
|
|
940
|
+
reason: EvaluationReason.INITIAL,
|
|
941
|
+
variableKey,
|
|
942
|
+
variableValue: variables[variableKey],
|
|
943
|
+
};
|
|
757
944
|
|
|
758
|
-
|
|
945
|
+
this.logger.debug("using initial variable", evaluation);
|
|
946
|
+
|
|
947
|
+
return evaluation;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
759
950
|
}
|
|
760
951
|
|
|
761
952
|
const feature = this.getFeature(featureKey);
|
|
@@ -842,7 +1033,7 @@ export class FeaturevisorInstance {
|
|
|
842
1033
|
}
|
|
843
1034
|
|
|
844
1035
|
// regular allocation
|
|
845
|
-
if (matchedAllocation && matchedAllocation.variation) {
|
|
1036
|
+
if (matchedAllocation && matchedAllocation.variation && Array.isArray(feature.variations)) {
|
|
846
1037
|
const variation = feature.variations.find((v) => v.value === matchedAllocation.variation);
|
|
847
1038
|
|
|
848
1039
|
if (variation && variation.variables) {
|