@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/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 = VariationType | VariableType;
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 in datafile", evaluation);
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
- // fall back to default
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.error("no matched variation", evaluation);
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] && this.stickyFeatures[key].variables) {
724
- const result = this.stickyFeatures[key].variables[variableKey];
906
+ if (this.stickyFeatures && this.stickyFeatures[key]) {
907
+ const variables = this.stickyFeatures[key].variables;
725
908
 
726
- if (typeof result !== "undefined") {
727
- evaluation = {
728
- featureKey: key,
729
- reason: EvaluationReason.STICKY,
730
- variableKey,
731
- variableValue: result,
732
- };
909
+ if (variables) {
910
+ const result = variables[variableKey];
733
911
 
734
- this.logger.debug("using sticky variable", evaluation);
912
+ if (typeof result !== "undefined") {
913
+ evaluation = {
914
+ featureKey: key,
915
+ reason: EvaluationReason.STICKY,
916
+ variableKey,
917
+ variableValue: result,
918
+ };
735
919
 
736
- return evaluation;
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
- evaluation = {
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
- this.logger.debug("using initial variable", evaluation);
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
- return evaluation;
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) {