@featurevisor/sdk 0.36.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 +22 -0
- package/README.md +14 -14
- package/coverage/clover.xml +378 -315
- package/coverage/coverage-final.json +7 -7
- package/coverage/lcov-report/bucket.ts.html +4 -4
- package/coverage/lcov-report/conditions.ts.html +38 -38
- 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 +80 -20
- package/coverage/lcov-report/index.html +28 -28
- package/coverage/lcov-report/instance.ts.html +754 -220
- package/coverage/lcov-report/logger.ts.html +3 -3
- package/coverage/lcov-report/segments.ts.html +12 -12
- package/coverage/lcov.info +661 -559
- package/dist/index.js +1 -1
- package/dist/index.js.gz +0 -0
- package/dist/index.js.map +1 -1
- package/lib/conditions.d.ts +3 -3
- package/lib/conditions.js +35 -35
- package/lib/conditions.js.map +1 -1
- package/lib/feature.d.ts +4 -3
- package/lib/feature.js +13 -5
- package/lib/feature.js.map +1 -1
- package/lib/instance.d.ts +36 -27
- package/lib/instance.js +289 -148
- package/lib/instance.js.map +1 -1
- package/lib/segments.d.ts +3 -3
- package/lib/segments.js +8 -8
- package/lib/segments.js.map +1 -1
- package/package.json +3 -3
- package/src/conditions.ts +37 -37
- package/src/feature.ts +26 -6
- package/src/instance.spec.ts +64 -61
- package/src/instance.ts +325 -147
- package/src/segments.ts +9 -9
package/src/instance.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
Context,
|
|
3
3
|
AttributeValue,
|
|
4
4
|
BucketKey,
|
|
5
5
|
BucketValue,
|
|
@@ -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
|
|
|
@@ -31,13 +32,13 @@ export type ReadyCallback = () => void;
|
|
|
31
32
|
export type ActivationCallback = (
|
|
32
33
|
featureName: string,
|
|
33
34
|
variation: VariationValue,
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
context: Context,
|
|
36
|
+
captureContext: Context,
|
|
36
37
|
) => void;
|
|
37
38
|
|
|
38
|
-
export type ConfigureBucketKey = (feature,
|
|
39
|
+
export type ConfigureBucketKey = (feature, context, bucketKey: BucketKey) => BucketKey;
|
|
39
40
|
|
|
40
|
-
export type ConfigureBucketValue = (feature,
|
|
41
|
+
export type ConfigureBucketValue = (feature, context, bucketValue: BucketValue) => BucketValue;
|
|
41
42
|
|
|
42
43
|
export interface Statuses {
|
|
43
44
|
ready: boolean;
|
|
@@ -46,6 +47,8 @@ export interface Statuses {
|
|
|
46
47
|
|
|
47
48
|
const DEFAULT_BUCKET_KEY_SEPARATOR = ".";
|
|
48
49
|
|
|
50
|
+
export type InterceptContext = (context: Context) => Context;
|
|
51
|
+
|
|
49
52
|
export interface InstanceOptions {
|
|
50
53
|
bucketKeySeparator?: string;
|
|
51
54
|
configureBucketKey?: ConfigureBucketKey;
|
|
@@ -54,7 +57,7 @@ export interface InstanceOptions {
|
|
|
54
57
|
datafileUrl?: string;
|
|
55
58
|
handleDatafileFetch?: (datafileUrl: string) => Promise<DatafileContent>;
|
|
56
59
|
initialFeatures?: InitialFeatures;
|
|
57
|
-
|
|
60
|
+
interceptContext?: InterceptContext;
|
|
58
61
|
logger?: Logger;
|
|
59
62
|
onActivation?: ActivationCallback;
|
|
60
63
|
onReady?: ReadyCallback;
|
|
@@ -76,6 +79,9 @@ export type DatafileFetchHandler = (datafileUrl: string) => Promise<DatafileCont
|
|
|
76
79
|
|
|
77
80
|
export enum EvaluationReason {
|
|
78
81
|
NOT_FOUND = "not_found",
|
|
82
|
+
NO_VARIATIONS = "no_variations",
|
|
83
|
+
DISABLED = "disabled",
|
|
84
|
+
OUT_OF_RANGE = "out_of_range",
|
|
79
85
|
FORCED = "forced",
|
|
80
86
|
INITIAL = "initial",
|
|
81
87
|
STICKY = "sticky",
|
|
@@ -95,6 +101,10 @@ export interface Evaluation {
|
|
|
95
101
|
bucketValue?: BucketValue;
|
|
96
102
|
ruleKey?: RuleKey;
|
|
97
103
|
error?: Error;
|
|
104
|
+
enabled?: boolean;
|
|
105
|
+
traffic?: Traffic;
|
|
106
|
+
sticky?: OverrideFeature;
|
|
107
|
+
initial?: OverrideFeature;
|
|
98
108
|
|
|
99
109
|
// variation
|
|
100
110
|
variation?: Variation;
|
|
@@ -117,7 +127,7 @@ function fetchDatafileContent(
|
|
|
117
127
|
return fetch(datafileUrl).then((res) => res.json());
|
|
118
128
|
}
|
|
119
129
|
|
|
120
|
-
type FieldType =
|
|
130
|
+
type FieldType = string | VariableType;
|
|
121
131
|
type ValueType = VariableValue;
|
|
122
132
|
|
|
123
133
|
export function getValueByType(value: ValueType, fieldType: FieldType): ValueType {
|
|
@@ -156,7 +166,7 @@ export class FeaturevisorInstance {
|
|
|
156
166
|
private datafileUrl?: string;
|
|
157
167
|
private handleDatafileFetch?: DatafileFetchHandler;
|
|
158
168
|
private initialFeatures?: InitialFeatures;
|
|
159
|
-
private
|
|
169
|
+
private interceptContext?: InterceptContext;
|
|
160
170
|
private logger: Logger;
|
|
161
171
|
private refreshInterval?: number; // seconds
|
|
162
172
|
private stickyFeatures?: StickyFeatures;
|
|
@@ -182,7 +192,7 @@ export class FeaturevisorInstance {
|
|
|
182
192
|
this.datafileUrl = options.datafileUrl;
|
|
183
193
|
this.handleDatafileFetch = options.handleDatafileFetch;
|
|
184
194
|
this.initialFeatures = options.initialFeatures;
|
|
185
|
-
this.
|
|
195
|
+
this.interceptContext = options.interceptContext;
|
|
186
196
|
this.logger = options.logger || createLogger();
|
|
187
197
|
this.refreshInterval = options.refreshInterval;
|
|
188
198
|
this.stickyFeatures = options.stickyFeatures;
|
|
@@ -281,7 +291,7 @@ export class FeaturevisorInstance {
|
|
|
281
291
|
/**
|
|
282
292
|
* Bucketing
|
|
283
293
|
*/
|
|
284
|
-
private getBucketKey(feature: Feature,
|
|
294
|
+
private getBucketKey(feature: Feature, context: Context): BucketKey {
|
|
285
295
|
const featureKey = feature.key;
|
|
286
296
|
|
|
287
297
|
let type;
|
|
@@ -305,7 +315,7 @@ export class FeaturevisorInstance {
|
|
|
305
315
|
const bucketKey: AttributeValue[] = [];
|
|
306
316
|
|
|
307
317
|
attributeKeys.forEach((attributeKey) => {
|
|
308
|
-
const attributeValue =
|
|
318
|
+
const attributeValue = context[attributeKey];
|
|
309
319
|
|
|
310
320
|
if (typeof attributeValue === "undefined") {
|
|
311
321
|
return;
|
|
@@ -326,19 +336,19 @@ export class FeaturevisorInstance {
|
|
|
326
336
|
const result = bucketKey.join(this.bucketKeySeparator);
|
|
327
337
|
|
|
328
338
|
if (this.configureBucketKey) {
|
|
329
|
-
return this.configureBucketKey(feature,
|
|
339
|
+
return this.configureBucketKey(feature, context, result);
|
|
330
340
|
}
|
|
331
341
|
|
|
332
342
|
return result;
|
|
333
343
|
}
|
|
334
344
|
|
|
335
|
-
private getBucketValue(feature: Feature,
|
|
336
|
-
const bucketKey = this.getBucketKey(feature,
|
|
345
|
+
private getBucketValue(feature: Feature, context: Context): BucketValue {
|
|
346
|
+
const bucketKey = this.getBucketKey(feature, context);
|
|
337
347
|
|
|
338
348
|
const value = getBucketedNumber(bucketKey);
|
|
339
349
|
|
|
340
350
|
if (this.configureBucketValue) {
|
|
341
|
-
return this.configureBucketValue(feature,
|
|
351
|
+
return this.configureBucketValue(feature, context, value);
|
|
342
352
|
}
|
|
343
353
|
|
|
344
354
|
return value;
|
|
@@ -416,15 +426,208 @@ export class FeaturevisorInstance {
|
|
|
416
426
|
clearInterval(this.intervalId);
|
|
417
427
|
}
|
|
418
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
|
+
|
|
419
609
|
/**
|
|
420
610
|
* Variation
|
|
421
611
|
*/
|
|
422
|
-
evaluateVariation(featureKey: FeatureKey | Feature,
|
|
612
|
+
evaluateVariation(featureKey: FeatureKey | Feature, context: Context = {}): Evaluation {
|
|
423
613
|
let evaluation: Evaluation;
|
|
424
614
|
|
|
425
615
|
try {
|
|
426
616
|
const key = typeof featureKey === "string" ? featureKey : featureKey.key;
|
|
427
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
|
+
|
|
428
631
|
// sticky
|
|
429
632
|
if (this.stickyFeatures && this.stickyFeatures[key]) {
|
|
430
633
|
const variationValue = this.stickyFeatures[key].variation;
|
|
@@ -472,17 +675,27 @@ export class FeaturevisorInstance {
|
|
|
472
675
|
reason: EvaluationReason.NOT_FOUND,
|
|
473
676
|
};
|
|
474
677
|
|
|
475
|
-
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);
|
|
476
691
|
|
|
477
692
|
return evaluation;
|
|
478
693
|
}
|
|
479
694
|
|
|
480
|
-
const
|
|
481
|
-
? this.interceptAttributes(attributes)
|
|
482
|
-
: attributes;
|
|
695
|
+
const finalContext = this.interceptContext ? this.interceptContext(context) : context;
|
|
483
696
|
|
|
484
697
|
// forced
|
|
485
|
-
const force = findForceFromFeature(feature,
|
|
698
|
+
const force = findForceFromFeature(feature, context, this.datafileReader);
|
|
486
699
|
|
|
487
700
|
if (force && force.variation) {
|
|
488
701
|
const variation = feature.variations.find((v) => v.value === force.variation);
|
|
@@ -501,11 +714,11 @@ export class FeaturevisorInstance {
|
|
|
501
714
|
}
|
|
502
715
|
|
|
503
716
|
// bucketing
|
|
504
|
-
const bucketValue = this.getBucketValue(feature,
|
|
717
|
+
const bucketValue = this.getBucketValue(feature, finalContext);
|
|
505
718
|
|
|
506
719
|
const { matchedTraffic, matchedAllocation } = getMatchedTrafficAndAllocation(
|
|
507
720
|
feature.traffic,
|
|
508
|
-
|
|
721
|
+
finalContext,
|
|
509
722
|
bucketValue,
|
|
510
723
|
this.datafileReader,
|
|
511
724
|
this.logger,
|
|
@@ -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) {
|
|
@@ -589,10 +786,10 @@ export class FeaturevisorInstance {
|
|
|
589
786
|
|
|
590
787
|
getVariation(
|
|
591
788
|
featureKey: FeatureKey | Feature,
|
|
592
|
-
|
|
789
|
+
context: Context = {},
|
|
593
790
|
): VariationValue | undefined {
|
|
594
791
|
try {
|
|
595
|
-
const evaluation = this.evaluateVariation(featureKey,
|
|
792
|
+
const evaluation = this.evaluateVariation(featureKey, context);
|
|
596
793
|
|
|
597
794
|
if (typeof evaluation.variationValue !== "undefined") {
|
|
598
795
|
return evaluation.variationValue;
|
|
@@ -610,48 +807,12 @@ export class FeaturevisorInstance {
|
|
|
610
807
|
}
|
|
611
808
|
}
|
|
612
809
|
|
|
613
|
-
getVariationBoolean(
|
|
614
|
-
featureKey: FeatureKey | Feature,
|
|
615
|
-
attributes: Attributes = {},
|
|
616
|
-
): boolean | undefined {
|
|
617
|
-
const variationValue = this.getVariation(featureKey, attributes);
|
|
618
|
-
|
|
619
|
-
return getValueByType(variationValue, "boolean") as boolean | undefined;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
getVariationString(
|
|
623
|
-
featureKey: FeatureKey | Feature,
|
|
624
|
-
attributes: Attributes = {},
|
|
625
|
-
): string | undefined {
|
|
626
|
-
const variationValue = this.getVariation(featureKey, attributes);
|
|
627
|
-
|
|
628
|
-
return getValueByType(variationValue, "string") as string | undefined;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
getVariationInteger(
|
|
632
|
-
featureKey: FeatureKey | Feature,
|
|
633
|
-
attributes: Attributes = {},
|
|
634
|
-
): number | undefined {
|
|
635
|
-
const variationValue = this.getVariation(featureKey, attributes);
|
|
636
|
-
|
|
637
|
-
return getValueByType(variationValue, "integer") as number | undefined;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
getVariationDouble(
|
|
641
|
-
featureKey: FeatureKey | Feature,
|
|
642
|
-
attributes: Attributes = {},
|
|
643
|
-
): number | undefined {
|
|
644
|
-
const variationValue = this.getVariation(featureKey, attributes);
|
|
645
|
-
|
|
646
|
-
return getValueByType(variationValue, "double") as number | undefined;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
810
|
/**
|
|
650
811
|
* Activate
|
|
651
812
|
*/
|
|
652
|
-
activate(featureKey: FeatureKey,
|
|
813
|
+
activate(featureKey: FeatureKey, context: Context = {}): VariationValue | undefined {
|
|
653
814
|
try {
|
|
654
|
-
const evaluation = this.evaluateVariation(featureKey,
|
|
815
|
+
const evaluation = this.evaluateVariation(featureKey, context);
|
|
655
816
|
const variationValue = evaluation.variation
|
|
656
817
|
? evaluation.variation.value
|
|
657
818
|
: evaluation.variationValue;
|
|
@@ -660,19 +821,17 @@ export class FeaturevisorInstance {
|
|
|
660
821
|
return undefined;
|
|
661
822
|
}
|
|
662
823
|
|
|
663
|
-
const
|
|
664
|
-
? this.interceptAttributes(attributes)
|
|
665
|
-
: attributes;
|
|
824
|
+
const finalContext = this.interceptContext ? this.interceptContext(context) : context;
|
|
666
825
|
|
|
667
|
-
const
|
|
826
|
+
const captureContext: Context = {};
|
|
668
827
|
|
|
669
828
|
const attributesForCapturing = this.datafileReader
|
|
670
829
|
.getAllAttributes()
|
|
671
830
|
.filter((a) => a.capture === true);
|
|
672
831
|
|
|
673
832
|
attributesForCapturing.forEach((a) => {
|
|
674
|
-
if (typeof
|
|
675
|
-
|
|
833
|
+
if (typeof finalContext[a.key] !== "undefined") {
|
|
834
|
+
captureContext[a.key] = context[a.key];
|
|
676
835
|
}
|
|
677
836
|
});
|
|
678
837
|
|
|
@@ -680,8 +839,8 @@ export class FeaturevisorInstance {
|
|
|
680
839
|
"activation",
|
|
681
840
|
featureKey,
|
|
682
841
|
variationValue,
|
|
683
|
-
|
|
684
|
-
|
|
842
|
+
finalContext,
|
|
843
|
+
captureContext,
|
|
685
844
|
evaluation,
|
|
686
845
|
);
|
|
687
846
|
|
|
@@ -693,26 +852,26 @@ export class FeaturevisorInstance {
|
|
|
693
852
|
}
|
|
694
853
|
}
|
|
695
854
|
|
|
696
|
-
activateBoolean(featureKey: FeatureKey,
|
|
697
|
-
const variationValue = this.activate(featureKey,
|
|
855
|
+
activateBoolean(featureKey: FeatureKey, context: Context = {}): boolean | undefined {
|
|
856
|
+
const variationValue = this.activate(featureKey, context);
|
|
698
857
|
|
|
699
858
|
return getValueByType(variationValue, "boolean") as boolean | undefined;
|
|
700
859
|
}
|
|
701
860
|
|
|
702
|
-
activateString(featureKey: FeatureKey,
|
|
703
|
-
const variationValue = this.activate(featureKey,
|
|
861
|
+
activateString(featureKey: FeatureKey, context: Context = {}): string | undefined {
|
|
862
|
+
const variationValue = this.activate(featureKey, context);
|
|
704
863
|
|
|
705
864
|
return getValueByType(variationValue, "string") as string | undefined;
|
|
706
865
|
}
|
|
707
866
|
|
|
708
|
-
activateInteger(featureKey: FeatureKey,
|
|
709
|
-
const variationValue = this.activate(featureKey,
|
|
867
|
+
activateInteger(featureKey: FeatureKey, context: Context = {}): number | undefined {
|
|
868
|
+
const variationValue = this.activate(featureKey, context);
|
|
710
869
|
|
|
711
870
|
return getValueByType(variationValue, "integer") as number | undefined;
|
|
712
871
|
}
|
|
713
872
|
|
|
714
|
-
activateDouble(featureKey: FeatureKey,
|
|
715
|
-
const variationValue = this.activate(featureKey,
|
|
873
|
+
activateDouble(featureKey: FeatureKey, context: Context = {}): number | undefined {
|
|
874
|
+
const variationValue = this.activate(featureKey, context);
|
|
716
875
|
|
|
717
876
|
return getValueByType(variationValue, "double") as number | undefined;
|
|
718
877
|
}
|
|
@@ -723,28 +882,45 @@ export class FeaturevisorInstance {
|
|
|
723
882
|
evaluateVariable(
|
|
724
883
|
featureKey: FeatureKey | Feature,
|
|
725
884
|
variableKey: VariableKey,
|
|
726
|
-
|
|
885
|
+
context: Context = {},
|
|
727
886
|
): Evaluation {
|
|
728
887
|
let evaluation: Evaluation;
|
|
729
888
|
|
|
730
889
|
try {
|
|
731
890
|
const key = typeof featureKey === "string" ? featureKey : featureKey.key;
|
|
732
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
|
+
|
|
733
905
|
// sticky
|
|
734
|
-
if (this.stickyFeatures && this.stickyFeatures[key]
|
|
735
|
-
const
|
|
906
|
+
if (this.stickyFeatures && this.stickyFeatures[key]) {
|
|
907
|
+
const variables = this.stickyFeatures[key].variables;
|
|
736
908
|
|
|
737
|
-
if (
|
|
738
|
-
|
|
739
|
-
featureKey: key,
|
|
740
|
-
reason: EvaluationReason.STICKY,
|
|
741
|
-
variableKey,
|
|
742
|
-
variableValue: result,
|
|
743
|
-
};
|
|
909
|
+
if (variables) {
|
|
910
|
+
const result = variables[variableKey];
|
|
744
911
|
|
|
745
|
-
|
|
912
|
+
if (typeof result !== "undefined") {
|
|
913
|
+
evaluation = {
|
|
914
|
+
featureKey: key,
|
|
915
|
+
reason: EvaluationReason.STICKY,
|
|
916
|
+
variableKey,
|
|
917
|
+
variableValue: result,
|
|
918
|
+
};
|
|
746
919
|
|
|
747
|
-
|
|
920
|
+
this.logger.debug("using sticky variable", evaluation);
|
|
921
|
+
|
|
922
|
+
return evaluation;
|
|
923
|
+
}
|
|
748
924
|
}
|
|
749
925
|
}
|
|
750
926
|
|
|
@@ -753,20 +929,24 @@ export class FeaturevisorInstance {
|
|
|
753
929
|
this.statuses &&
|
|
754
930
|
!this.statuses.ready &&
|
|
755
931
|
this.initialFeatures &&
|
|
756
|
-
this.initialFeatures[key]
|
|
757
|
-
this.initialFeatures[key].variables &&
|
|
758
|
-
typeof this.initialFeatures[key].variables[variableKey] !== "undefined"
|
|
932
|
+
this.initialFeatures[key]
|
|
759
933
|
) {
|
|
760
|
-
|
|
761
|
-
featureKey: key,
|
|
762
|
-
reason: EvaluationReason.INITIAL,
|
|
763
|
-
variableKey,
|
|
764
|
-
variableValue: this.initialFeatures[key].variables[variableKey],
|
|
765
|
-
};
|
|
934
|
+
const variables = this.initialFeatures[key].variables;
|
|
766
935
|
|
|
767
|
-
|
|
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
|
+
};
|
|
768
944
|
|
|
769
|
-
|
|
945
|
+
this.logger.debug("using initial variable", evaluation);
|
|
946
|
+
|
|
947
|
+
return evaluation;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
770
950
|
}
|
|
771
951
|
|
|
772
952
|
const feature = this.getFeature(featureKey);
|
|
@@ -801,12 +981,10 @@ export class FeaturevisorInstance {
|
|
|
801
981
|
return evaluation;
|
|
802
982
|
}
|
|
803
983
|
|
|
804
|
-
const
|
|
805
|
-
? this.interceptAttributes(attributes)
|
|
806
|
-
: attributes;
|
|
984
|
+
const finalContext = this.interceptContext ? this.interceptContext(context) : context;
|
|
807
985
|
|
|
808
986
|
// forced
|
|
809
|
-
const force = findForceFromFeature(feature,
|
|
987
|
+
const force = findForceFromFeature(feature, context, this.datafileReader);
|
|
810
988
|
|
|
811
989
|
if (force && force.variables && typeof force.variables[variableKey] !== "undefined") {
|
|
812
990
|
evaluation = {
|
|
@@ -823,11 +1001,11 @@ export class FeaturevisorInstance {
|
|
|
823
1001
|
}
|
|
824
1002
|
|
|
825
1003
|
// bucketing
|
|
826
|
-
const bucketValue = this.getBucketValue(feature,
|
|
1004
|
+
const bucketValue = this.getBucketValue(feature, finalContext);
|
|
827
1005
|
|
|
828
1006
|
const { matchedTraffic, matchedAllocation } = getMatchedTrafficAndAllocation(
|
|
829
1007
|
feature.traffic,
|
|
830
|
-
|
|
1008
|
+
finalContext,
|
|
831
1009
|
bucketValue,
|
|
832
1010
|
this.datafileReader,
|
|
833
1011
|
this.logger,
|
|
@@ -855,7 +1033,7 @@ export class FeaturevisorInstance {
|
|
|
855
1033
|
}
|
|
856
1034
|
|
|
857
1035
|
// regular allocation
|
|
858
|
-
if (matchedAllocation && matchedAllocation.variation) {
|
|
1036
|
+
if (matchedAllocation && matchedAllocation.variation && Array.isArray(feature.variations)) {
|
|
859
1037
|
const variation = feature.variations.find((v) => v.value === matchedAllocation.variation);
|
|
860
1038
|
|
|
861
1039
|
if (variation && variation.variables) {
|
|
@@ -867,7 +1045,7 @@ export class FeaturevisorInstance {
|
|
|
867
1045
|
if (o.conditions) {
|
|
868
1046
|
return allConditionsAreMatched(
|
|
869
1047
|
typeof o.conditions === "string" ? JSON.parse(o.conditions) : o.conditions,
|
|
870
|
-
|
|
1048
|
+
finalContext,
|
|
871
1049
|
);
|
|
872
1050
|
}
|
|
873
1051
|
|
|
@@ -876,7 +1054,7 @@ export class FeaturevisorInstance {
|
|
|
876
1054
|
typeof o.segments === "string" && o.segments !== "*"
|
|
877
1055
|
? JSON.parse(o.segments)
|
|
878
1056
|
: o.segments,
|
|
879
|
-
|
|
1057
|
+
finalContext,
|
|
880
1058
|
this.datafileReader,
|
|
881
1059
|
);
|
|
882
1060
|
}
|
|
@@ -949,10 +1127,10 @@ export class FeaturevisorInstance {
|
|
|
949
1127
|
getVariable(
|
|
950
1128
|
featureKey: FeatureKey | Feature,
|
|
951
1129
|
variableKey: string,
|
|
952
|
-
|
|
1130
|
+
context: Context = {},
|
|
953
1131
|
): VariableValue | undefined {
|
|
954
1132
|
try {
|
|
955
|
-
const evaluation = this.evaluateVariable(featureKey, variableKey,
|
|
1133
|
+
const evaluation = this.evaluateVariable(featureKey, variableKey, context);
|
|
956
1134
|
|
|
957
1135
|
if (typeof evaluation.variableValue !== "undefined") {
|
|
958
1136
|
if (
|
|
@@ -977,9 +1155,9 @@ export class FeaturevisorInstance {
|
|
|
977
1155
|
getVariableBoolean(
|
|
978
1156
|
featureKey: FeatureKey | Feature,
|
|
979
1157
|
variableKey: string,
|
|
980
|
-
|
|
1158
|
+
context: Context = {},
|
|
981
1159
|
): boolean | undefined {
|
|
982
|
-
const variableValue = this.getVariable(featureKey, variableKey,
|
|
1160
|
+
const variableValue = this.getVariable(featureKey, variableKey, context);
|
|
983
1161
|
|
|
984
1162
|
return getValueByType(variableValue, "boolean") as boolean | undefined;
|
|
985
1163
|
}
|
|
@@ -987,9 +1165,9 @@ export class FeaturevisorInstance {
|
|
|
987
1165
|
getVariableString(
|
|
988
1166
|
featureKey: FeatureKey | Feature,
|
|
989
1167
|
variableKey: string,
|
|
990
|
-
|
|
1168
|
+
context: Context = {},
|
|
991
1169
|
): string | undefined {
|
|
992
|
-
const variableValue = this.getVariable(featureKey, variableKey,
|
|
1170
|
+
const variableValue = this.getVariable(featureKey, variableKey, context);
|
|
993
1171
|
|
|
994
1172
|
return getValueByType(variableValue, "string") as string | undefined;
|
|
995
1173
|
}
|
|
@@ -997,9 +1175,9 @@ export class FeaturevisorInstance {
|
|
|
997
1175
|
getVariableInteger(
|
|
998
1176
|
featureKey: FeatureKey | Feature,
|
|
999
1177
|
variableKey: string,
|
|
1000
|
-
|
|
1178
|
+
context: Context = {},
|
|
1001
1179
|
): number | undefined {
|
|
1002
|
-
const variableValue = this.getVariable(featureKey, variableKey,
|
|
1180
|
+
const variableValue = this.getVariable(featureKey, variableKey, context);
|
|
1003
1181
|
|
|
1004
1182
|
return getValueByType(variableValue, "integer") as number | undefined;
|
|
1005
1183
|
}
|
|
@@ -1007,9 +1185,9 @@ export class FeaturevisorInstance {
|
|
|
1007
1185
|
getVariableDouble(
|
|
1008
1186
|
featureKey: FeatureKey | Feature,
|
|
1009
1187
|
variableKey: string,
|
|
1010
|
-
|
|
1188
|
+
context: Context = {},
|
|
1011
1189
|
): number | undefined {
|
|
1012
|
-
const variableValue = this.getVariable(featureKey, variableKey,
|
|
1190
|
+
const variableValue = this.getVariable(featureKey, variableKey, context);
|
|
1013
1191
|
|
|
1014
1192
|
return getValueByType(variableValue, "double") as number | undefined;
|
|
1015
1193
|
}
|
|
@@ -1017,9 +1195,9 @@ export class FeaturevisorInstance {
|
|
|
1017
1195
|
getVariableArray(
|
|
1018
1196
|
featureKey: FeatureKey | Feature,
|
|
1019
1197
|
variableKey: string,
|
|
1020
|
-
|
|
1198
|
+
context: Context = {},
|
|
1021
1199
|
): string[] | undefined {
|
|
1022
|
-
const variableValue = this.getVariable(featureKey, variableKey,
|
|
1200
|
+
const variableValue = this.getVariable(featureKey, variableKey, context);
|
|
1023
1201
|
|
|
1024
1202
|
return getValueByType(variableValue, "array") as string[] | undefined;
|
|
1025
1203
|
}
|
|
@@ -1027,9 +1205,9 @@ export class FeaturevisorInstance {
|
|
|
1027
1205
|
getVariableObject<T>(
|
|
1028
1206
|
featureKey: FeatureKey | Feature,
|
|
1029
1207
|
variableKey: string,
|
|
1030
|
-
|
|
1208
|
+
context: Context = {},
|
|
1031
1209
|
): T | undefined {
|
|
1032
|
-
const variableValue = this.getVariable(featureKey, variableKey,
|
|
1210
|
+
const variableValue = this.getVariable(featureKey, variableKey, context);
|
|
1033
1211
|
|
|
1034
1212
|
return getValueByType(variableValue, "object") as T | undefined;
|
|
1035
1213
|
}
|
|
@@ -1037,9 +1215,9 @@ export class FeaturevisorInstance {
|
|
|
1037
1215
|
getVariableJSON<T>(
|
|
1038
1216
|
featureKey: FeatureKey | Feature,
|
|
1039
1217
|
variableKey: string,
|
|
1040
|
-
|
|
1218
|
+
context: Context = {},
|
|
1041
1219
|
): T | undefined {
|
|
1042
|
-
const variableValue = this.getVariable(featureKey, variableKey,
|
|
1220
|
+
const variableValue = this.getVariable(featureKey, variableKey, context);
|
|
1043
1221
|
|
|
1044
1222
|
return getValueByType(variableValue, "json") as T | undefined;
|
|
1045
1223
|
}
|