@featurevisor/sdk 0.34.0 → 0.35.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
@@ -12,18 +12,19 @@ import {
12
12
  VariableValue,
13
13
  VariationType,
14
14
  VariationValue,
15
+ Variation,
16
+ RuleKey,
17
+ VariableKey,
18
+ VariableSchema,
15
19
  } from "@featurevisor/types";
16
20
 
17
21
  import { createLogger, Logger } from "./logger";
18
22
  import { DatafileReader } from "./datafileReader";
19
23
  import { Emitter } from "./emitter";
20
24
  import { getBucketedNumber } from "./bucket";
21
- import {
22
- getBucketedVariation,
23
- getBucketedVariableValue,
24
- getForcedVariation,
25
- getForcedVariableValue,
26
- } from "./feature";
25
+ import { findForceFromFeature, getMatchedTrafficAndAllocation } from "./feature";
26
+ import { allConditionsAreMatched } from "./conditions";
27
+ import { allGroupSegmentsAreMatched } from "./segments";
27
28
 
28
29
  export type ReadyCallback = () => void;
29
30
 
@@ -73,6 +74,38 @@ const emptyDatafile: DatafileContent = {
73
74
 
74
75
  export type DatafileFetchHandler = (datafileUrl: string) => Promise<DatafileContent>;
75
76
 
77
+ export enum EvaluationReason {
78
+ NOT_FOUND = "not_found",
79
+ FORCED = "forced",
80
+ INITIAL = "initial",
81
+ STICKY = "sticky",
82
+ RULE = "rule",
83
+ ALLOCATED = "allocated",
84
+ DEFAULTED = "defaulted",
85
+ OVERRIDE = "override",
86
+ ERROR = "error",
87
+ }
88
+
89
+ export interface Evaluation {
90
+ // required
91
+ featureKey: FeatureKey;
92
+ reason: EvaluationReason;
93
+
94
+ // common
95
+ bucketValue?: BucketValue;
96
+ ruleKey?: RuleKey;
97
+ error?: Error;
98
+
99
+ // variation
100
+ variation?: Variation;
101
+ variationValue?: VariationValue;
102
+
103
+ // variable
104
+ variableKey?: VariableKey;
105
+ variableValue?: VariableValue;
106
+ variableSchema?: VariableSchema;
107
+ }
108
+
76
109
  function fetchDatafileContent(
77
110
  datafileUrl,
78
111
  handleDatafileFetch?: DatafileFetchHandler,
@@ -88,26 +121,30 @@ type FieldType = VariationType | VariableType;
88
121
  type ValueType = VariableValue;
89
122
 
90
123
  export function getValueByType(value: ValueType, fieldType: FieldType): ValueType {
91
- if (value === undefined) {
92
- return undefined;
93
- }
124
+ try {
125
+ if (value === undefined) {
126
+ return undefined;
127
+ }
94
128
 
95
- switch (fieldType) {
96
- case "string":
97
- return typeof value === "string" ? value : undefined;
98
- case "integer":
99
- return parseInt(value as string, 10);
100
- case "double":
101
- return parseFloat(value as string);
102
- case "boolean":
103
- return value === true;
104
- case "array":
105
- return Array.isArray(value) ? value : undefined;
106
- case "object":
107
- return typeof value === "object" ? value : undefined;
108
- // @NOTE: `json` is not handled here intentionally
109
- default:
110
- return value;
129
+ switch (fieldType) {
130
+ case "string":
131
+ return typeof value === "string" ? value : undefined;
132
+ case "integer":
133
+ return parseInt(value as string, 10);
134
+ case "double":
135
+ return parseFloat(value as string);
136
+ case "boolean":
137
+ return value === true;
138
+ case "array":
139
+ return Array.isArray(value) ? value : undefined;
140
+ case "object":
141
+ return typeof value === "object" ? value : undefined;
142
+ // @NOTE: `json` is not handled here intentionally
143
+ default:
144
+ return value;
145
+ }
146
+ } catch (e) {
147
+ return undefined;
111
148
  }
112
149
  }
113
150
 
@@ -382,89 +419,190 @@ export class FeaturevisorInstance {
382
419
  /**
383
420
  * Variation
384
421
  */
422
+ evaluateVariation(featureKey: FeatureKey | Feature, attributes: Attributes = {}): Evaluation {
423
+ let evaluation: Evaluation;
385
424
 
386
- getVariation(
387
- featureKey: FeatureKey | Feature,
388
- attributes: Attributes = {},
389
- ): VariationValue | undefined {
390
425
  try {
391
426
  const key = typeof featureKey === "string" ? featureKey : featureKey.key;
392
427
 
428
+ // sticky
393
429
  if (this.stickyFeatures && this.stickyFeatures[key]) {
394
- const result = this.stickyFeatures[key].variation;
430
+ const variationValue = this.stickyFeatures[key].variation;
395
431
 
396
- if (typeof result !== "undefined") {
397
- this.logger.debug("using sticky variation", {
432
+ if (typeof variationValue !== "undefined") {
433
+ evaluation = {
398
434
  featureKey: key,
399
- variation: result,
400
- });
435
+ reason: EvaluationReason.STICKY,
436
+ variationValue,
437
+ };
401
438
 
402
- return result;
439
+ this.logger.debug("using sticky variation", evaluation);
440
+
441
+ return evaluation;
403
442
  }
404
443
  }
405
444
 
445
+ // initial
406
446
  if (
407
447
  this.statuses &&
408
448
  !this.statuses.ready &&
409
449
  this.initialFeatures &&
410
- this.initialFeatures[key]
450
+ this.initialFeatures[key] &&
451
+ typeof this.initialFeatures[key].variation !== "undefined"
411
452
  ) {
412
- const result = this.initialFeatures[key].variation;
453
+ const variationValue = this.initialFeatures[key].variation;
413
454
 
414
- if (typeof result !== "undefined") {
415
- this.logger.debug("using initial variation", {
416
- featureKey: key,
417
- variation: result,
418
- });
455
+ evaluation = {
456
+ featureKey: key,
457
+ reason: EvaluationReason.INITIAL,
458
+ variationValue,
459
+ };
419
460
 
420
- return result;
421
- }
461
+ this.logger.debug("using initial variation", evaluation);
462
+
463
+ return evaluation;
422
464
  }
423
465
 
424
466
  const feature = this.getFeature(featureKey);
425
467
 
468
+ // not found
426
469
  if (!feature) {
427
- this.logger.warn("feature not found in datafile", { featureKey });
470
+ evaluation = {
471
+ featureKey: key,
472
+ reason: EvaluationReason.NOT_FOUND,
473
+ };
428
474
 
429
- return undefined;
475
+ this.logger.warn("feature not found in datafile", evaluation);
476
+
477
+ return evaluation;
430
478
  }
431
479
 
432
480
  const finalAttributes = this.interceptAttributes
433
481
  ? this.interceptAttributes(attributes)
434
482
  : attributes;
435
483
 
436
- const forcedVariation = getForcedVariation(feature, finalAttributes, this.datafileReader);
484
+ // forced
485
+ const force = findForceFromFeature(feature, attributes, this.datafileReader);
437
486
 
438
- if (forcedVariation) {
439
- this.logger.debug("forced variation found", {
440
- featureKey,
441
- variation: forcedVariation.value,
442
- });
487
+ if (force && force.variation) {
488
+ const variation = feature.variations.find((v) => v.value === force.variation);
489
+
490
+ if (variation) {
491
+ evaluation = {
492
+ featureKey: feature.key,
493
+ reason: EvaluationReason.FORCED,
494
+ variation,
495
+ };
443
496
 
444
- return forcedVariation.value;
497
+ this.logger.debug("forced variation found", evaluation);
498
+
499
+ return evaluation;
500
+ }
445
501
  }
446
502
 
503
+ // bucketing
447
504
  const bucketValue = this.getBucketValue(feature, finalAttributes);
448
505
 
449
- const variation = getBucketedVariation(
450
- feature,
506
+ const { matchedTraffic, matchedAllocation } = getMatchedTrafficAndAllocation(
507
+ feature.traffic,
451
508
  finalAttributes,
452
509
  bucketValue,
453
510
  this.datafileReader,
454
511
  this.logger,
455
512
  );
456
513
 
457
- if (!variation) {
458
- this.logger.debug("using default variation", {
459
- featureKey,
514
+ if (matchedTraffic) {
515
+ // override from rule
516
+ if (matchedTraffic.variation) {
517
+ const variation = feature.variations.find((v) => v.value === matchedTraffic.variation);
518
+
519
+ if (variation) {
520
+ evaluation = {
521
+ featureKey: feature.key,
522
+ reason: EvaluationReason.RULE,
523
+ variation,
524
+ bucketValue,
525
+ ruleKey: matchedTraffic.key,
526
+ };
527
+
528
+ this.logger.debug("override from rule", evaluation);
529
+
530
+ return evaluation;
531
+ }
532
+ }
533
+
534
+ // regular allocation
535
+ if (matchedAllocation && matchedAllocation.variation) {
536
+ const variation = feature.variations.find((v) => v.value === matchedAllocation.variation);
537
+
538
+ if (variation) {
539
+ evaluation = {
540
+ featureKey: feature.key,
541
+ reason: EvaluationReason.ALLOCATED,
542
+ bucketValue,
543
+ variation,
544
+ };
545
+
546
+ this.logger.debug("allocated variation", evaluation);
547
+
548
+ return evaluation;
549
+ }
550
+ }
551
+ }
552
+
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,
460
560
  bucketValue,
461
- variation: feature.defaultVariation,
462
- });
561
+ variation,
562
+ };
463
563
 
464
- return feature.defaultVariation;
564
+ this.logger.debug("using default variation", evaluation);
565
+
566
+ return evaluation;
465
567
  }
466
568
 
467
- return variation.value;
569
+ // nothing matched (this should never happen)
570
+ evaluation = {
571
+ featureKey: feature.key,
572
+ reason: EvaluationReason.ERROR,
573
+ bucketValue,
574
+ };
575
+
576
+ this.logger.error("no matched variation", evaluation);
577
+
578
+ return evaluation;
579
+ } catch (e) {
580
+ evaluation = {
581
+ featureKey: typeof featureKey === "string" ? featureKey : featureKey.key,
582
+ reason: EvaluationReason.ERROR,
583
+ error: e,
584
+ };
585
+
586
+ return evaluation;
587
+ }
588
+ }
589
+
590
+ getVariation(
591
+ featureKey: FeatureKey | Feature,
592
+ attributes: Attributes = {},
593
+ ): VariationValue | undefined {
594
+ try {
595
+ const evaluation = this.evaluateVariation(featureKey, attributes);
596
+
597
+ if (typeof evaluation.variationValue !== "undefined") {
598
+ return evaluation.variationValue;
599
+ }
600
+
601
+ if (evaluation.variation) {
602
+ return evaluation.variation.value;
603
+ }
604
+
605
+ return undefined;
468
606
  } catch (e) {
469
607
  this.logger.error("getVariation", { featureKey, error: e });
470
608
 
@@ -513,7 +651,10 @@ export class FeaturevisorInstance {
513
651
  */
514
652
  activate(featureKey: FeatureKey, attributes: Attributes = {}): VariationValue | undefined {
515
653
  try {
516
- const variationValue = this.getVariation(featureKey, attributes);
654
+ const evaluation = this.evaluateVariation(featureKey, attributes);
655
+ const variationValue = evaluation.variation
656
+ ? evaluation.variation.value
657
+ : evaluation.variationValue;
517
658
 
518
659
  if (typeof variationValue === "undefined") {
519
660
  return undefined;
@@ -541,6 +682,7 @@ export class FeaturevisorInstance {
541
682
  variationValue,
542
683
  finalAttributes,
543
684
  captureAttributes,
685
+ evaluation,
544
686
  );
545
687
 
546
688
  return variationValue;
@@ -578,92 +720,253 @@ export class FeaturevisorInstance {
578
720
  /**
579
721
  * Variable
580
722
  */
581
-
582
- getVariable(
723
+ evaluateVariable(
583
724
  featureKey: FeatureKey | Feature,
584
- variableKey: string,
725
+ variableKey: VariableKey,
585
726
  attributes: Attributes = {},
586
- ): VariableValue | undefined {
727
+ ): Evaluation {
728
+ let evaluation: Evaluation;
729
+
587
730
  try {
588
731
  const key = typeof featureKey === "string" ? featureKey : featureKey.key;
589
732
 
733
+ // sticky
590
734
  if (this.stickyFeatures && this.stickyFeatures[key] && this.stickyFeatures[key].variables) {
591
735
  const result = this.stickyFeatures[key].variables[variableKey];
592
736
 
593
737
  if (typeof result !== "undefined") {
594
- this.logger.debug("using sticky variable", {
738
+ evaluation = {
595
739
  featureKey: key,
740
+ reason: EvaluationReason.STICKY,
596
741
  variableKey,
597
- });
742
+ variableValue: result,
743
+ };
744
+
745
+ this.logger.debug("using sticky variable", evaluation);
598
746
 
599
- return result;
747
+ return evaluation;
600
748
  }
601
749
  }
602
750
 
751
+ // initial
603
752
  if (
604
753
  this.statuses &&
605
754
  !this.statuses.ready &&
606
755
  this.initialFeatures &&
607
756
  this.initialFeatures[key] &&
608
- this.initialFeatures[key].variables
757
+ this.initialFeatures[key].variables &&
758
+ typeof this.initialFeatures[key].variables[variableKey] !== "undefined"
609
759
  ) {
610
- const result = this.initialFeatures[key].variables[variableKey];
760
+ evaluation = {
761
+ featureKey: key,
762
+ reason: EvaluationReason.INITIAL,
763
+ variableKey,
764
+ variableValue: this.initialFeatures[key].variables[variableKey],
765
+ };
611
766
 
612
- if (typeof result !== "undefined") {
613
- this.logger.debug("using initial variable", {
614
- featureKey: key,
615
- variableKey,
616
- });
767
+ this.logger.debug("using initial variable", evaluation);
617
768
 
618
- return result;
619
- }
769
+ return evaluation;
620
770
  }
621
771
 
622
772
  const feature = this.getFeature(featureKey);
623
773
 
774
+ // not found
624
775
  if (!feature) {
625
- this.logger.warn("feature not found in datafile", { featureKey, variableKey });
776
+ evaluation = {
777
+ featureKey: key,
778
+ reason: EvaluationReason.NOT_FOUND,
779
+ variableKey,
780
+ };
626
781
 
627
- return undefined;
782
+ this.logger.warn("feature not found in datafile", evaluation);
783
+
784
+ return evaluation;
628
785
  }
629
786
 
630
787
  const variableSchema = Array.isArray(feature.variablesSchema)
631
788
  ? feature.variablesSchema.find((v) => v.key === variableKey)
632
789
  : undefined;
633
790
 
791
+ // variable schema not found
634
792
  if (!variableSchema) {
635
- this.logger.warn("variable schema not found", { featureKey, variableKey });
793
+ evaluation = {
794
+ featureKey: key,
795
+ reason: EvaluationReason.NOT_FOUND,
796
+ variableKey,
797
+ };
636
798
 
637
- return undefined;
799
+ this.logger.warn("variable schema not found", evaluation);
800
+
801
+ return evaluation;
638
802
  }
639
803
 
640
804
  const finalAttributes = this.interceptAttributes
641
805
  ? this.interceptAttributes(attributes)
642
806
  : attributes;
643
807
 
644
- const forcedVariableValue = getForcedVariableValue(
645
- feature,
646
- variableSchema,
647
- finalAttributes,
648
- this.datafileReader,
649
- );
808
+ // forced
809
+ const force = findForceFromFeature(feature, attributes, this.datafileReader);
650
810
 
651
- if (typeof forcedVariableValue !== "undefined") {
652
- this.logger.debug("forced variable value found", { featureKey, variableKey });
811
+ if (force && force.variables && typeof force.variables[variableKey] !== "undefined") {
812
+ evaluation = {
813
+ featureKey: feature.key,
814
+ reason: EvaluationReason.FORCED,
815
+ variableKey,
816
+ variableSchema,
817
+ variableValue: force.variables[variableKey],
818
+ };
653
819
 
654
- return forcedVariableValue;
820
+ this.logger.debug("forced variable", evaluation);
821
+
822
+ return evaluation;
655
823
  }
656
824
 
825
+ // bucketing
657
826
  const bucketValue = this.getBucketValue(feature, finalAttributes);
658
827
 
659
- return getBucketedVariableValue(
660
- feature,
661
- variableSchema,
828
+ const { matchedTraffic, matchedAllocation } = getMatchedTrafficAndAllocation(
829
+ feature.traffic,
662
830
  finalAttributes,
663
831
  bucketValue,
664
832
  this.datafileReader,
665
833
  this.logger,
666
834
  );
835
+
836
+ if (matchedTraffic) {
837
+ // override from rule
838
+ if (
839
+ matchedTraffic.variables &&
840
+ typeof matchedTraffic.variables[variableKey] !== "undefined"
841
+ ) {
842
+ evaluation = {
843
+ featureKey: feature.key,
844
+ reason: EvaluationReason.RULE,
845
+ variableKey,
846
+ variableSchema,
847
+ variableValue: matchedTraffic.variables[variableKey],
848
+ bucketValue,
849
+ ruleKey: matchedTraffic.key,
850
+ };
851
+
852
+ this.logger.debug("override from rule", evaluation);
853
+
854
+ return evaluation;
855
+ }
856
+
857
+ // regular allocation
858
+ if (matchedAllocation && matchedAllocation.variation) {
859
+ const variation = feature.variations.find((v) => v.value === matchedAllocation.variation);
860
+
861
+ if (variation && variation.variables) {
862
+ const variableFromVariation = variation.variables.find((v) => v.key === variableKey);
863
+
864
+ if (variableFromVariation) {
865
+ if (variableFromVariation.overrides) {
866
+ const override = variableFromVariation.overrides.find((o) => {
867
+ if (o.conditions) {
868
+ return allConditionsAreMatched(
869
+ typeof o.conditions === "string" ? JSON.parse(o.conditions) : o.conditions,
870
+ finalAttributes,
871
+ );
872
+ }
873
+
874
+ if (o.segments) {
875
+ return allGroupSegmentsAreMatched(
876
+ typeof o.segments === "string" && o.segments !== "*"
877
+ ? JSON.parse(o.segments)
878
+ : o.segments,
879
+ finalAttributes,
880
+ this.datafileReader,
881
+ );
882
+ }
883
+
884
+ return false;
885
+ });
886
+
887
+ if (override) {
888
+ evaluation = {
889
+ featureKey: feature.key,
890
+ reason: EvaluationReason.OVERRIDE,
891
+ variableKey,
892
+ variableSchema,
893
+ variableValue: override.value,
894
+ bucketValue,
895
+ ruleKey: matchedTraffic.key,
896
+ };
897
+
898
+ this.logger.debug("variable override", evaluation);
899
+
900
+ return evaluation;
901
+ }
902
+ }
903
+
904
+ if (typeof variableFromVariation.value !== "undefined") {
905
+ evaluation = {
906
+ featureKey: feature.key,
907
+ reason: EvaluationReason.ALLOCATED,
908
+ variableKey,
909
+ variableSchema,
910
+ variableValue: variableFromVariation.value,
911
+ bucketValue,
912
+ ruleKey: matchedTraffic.key,
913
+ };
914
+
915
+ this.logger.debug("allocated variable", evaluation);
916
+
917
+ return evaluation;
918
+ }
919
+ }
920
+ }
921
+ }
922
+ }
923
+
924
+ // fall back to default
925
+ evaluation = {
926
+ featureKey: feature.key,
927
+ reason: EvaluationReason.DEFAULTED,
928
+ variableKey,
929
+ variableSchema,
930
+ variableValue: variableSchema.defaultValue,
931
+ bucketValue,
932
+ };
933
+
934
+ this.logger.debug("using default value", evaluation);
935
+
936
+ return evaluation;
937
+ } catch (e) {
938
+ evaluation = {
939
+ featureKey: typeof featureKey === "string" ? featureKey : featureKey.key,
940
+ reason: EvaluationReason.ERROR,
941
+ variableKey,
942
+ error: e,
943
+ };
944
+
945
+ return evaluation;
946
+ }
947
+ }
948
+
949
+ getVariable(
950
+ featureKey: FeatureKey | Feature,
951
+ variableKey: string,
952
+ attributes: Attributes = {},
953
+ ): VariableValue | undefined {
954
+ try {
955
+ const evaluation = this.evaluateVariable(featureKey, variableKey, attributes);
956
+
957
+ if (typeof evaluation.variableValue !== "undefined") {
958
+ if (
959
+ evaluation.variableSchema &&
960
+ evaluation.variableSchema.type === "json" &&
961
+ typeof evaluation.variableValue === "string"
962
+ ) {
963
+ return JSON.parse(evaluation.variableValue);
964
+ }
965
+
966
+ return evaluation.variableValue;
967
+ }
968
+
969
+ return undefined;
667
970
  } catch (e) {
668
971
  this.logger.error("getVariable", { featureKey, variableKey, error: e });
669
972