@featurevisor/sdk 1.35.3 → 2.0.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +2 -381
  3. package/coverage/clover.xml +707 -645
  4. package/coverage/coverage-final.json +11 -9
  5. package/coverage/lcov-report/{segments.ts.html → bucketer.ts.html} +155 -77
  6. package/coverage/lcov-report/child.ts.html +940 -0
  7. package/coverage/lcov-report/conditions.ts.html +107 -158
  8. package/coverage/lcov-report/datafileReader.ts.html +763 -103
  9. package/coverage/lcov-report/emitter.ts.html +77 -59
  10. package/coverage/lcov-report/evaluate.ts.html +689 -416
  11. package/coverage/lcov-report/events.ts.html +334 -0
  12. package/coverage/lcov-report/helpers.ts.html +184 -0
  13. package/coverage/lcov-report/{bucket.ts.html → hooks.ts.html} +86 -239
  14. package/coverage/lcov-report/index.html +119 -89
  15. package/coverage/lcov-report/instance.ts.html +341 -773
  16. package/coverage/lcov-report/logger.ts.html +64 -64
  17. package/coverage/lcov.info +1433 -1226
  18. package/dist/bucketer.d.ts +11 -0
  19. package/dist/child.d.ts +26 -0
  20. package/dist/compareVersions.d.ts +4 -0
  21. package/dist/conditions.d.ts +4 -4
  22. package/dist/datafileReader.d.ts +26 -6
  23. package/dist/emitter.d.ts +8 -9
  24. package/dist/evaluate.d.ts +31 -29
  25. package/dist/events.d.ts +5 -0
  26. package/dist/helpers.d.ts +5 -0
  27. package/dist/hooks.d.ts +45 -0
  28. package/dist/index.d.ts +3 -2
  29. package/dist/index.js +1 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.mjs +1 -1
  32. package/dist/index.mjs.gz +0 -0
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/instance.d.ts +40 -72
  35. package/dist/logger.d.ts +6 -5
  36. package/dist/murmurhash.d.ts +1 -0
  37. package/jest.config.js +2 -0
  38. package/lib/bucketer.d.ts +11 -0
  39. package/lib/child.d.ts +26 -0
  40. package/lib/compareVersions.d.ts +4 -0
  41. package/lib/conditions.d.ts +4 -4
  42. package/lib/datafileReader.d.ts +26 -6
  43. package/lib/emitter.d.ts +8 -9
  44. package/lib/evaluate.d.ts +31 -29
  45. package/lib/events.d.ts +5 -0
  46. package/lib/helpers.d.ts +5 -0
  47. package/lib/hooks.d.ts +45 -0
  48. package/lib/index.d.ts +3 -2
  49. package/lib/instance.d.ts +40 -72
  50. package/lib/logger.d.ts +6 -5
  51. package/lib/murmurhash.d.ts +1 -0
  52. package/package.json +3 -5
  53. package/src/bucketer.spec.ts +165 -0
  54. package/src/bucketer.ts +84 -0
  55. package/src/child.spec.ts +267 -0
  56. package/src/child.ts +285 -0
  57. package/src/compareVersions.ts +93 -0
  58. package/src/conditions.spec.ts +563 -353
  59. package/src/conditions.ts +46 -63
  60. package/src/datafileReader.spec.ts +396 -84
  61. package/src/datafileReader.ts +280 -60
  62. package/src/emitter.spec.ts +27 -86
  63. package/src/emitter.ts +38 -32
  64. package/src/evaluate.ts +349 -258
  65. package/src/events.spec.ts +154 -0
  66. package/src/events.ts +83 -0
  67. package/src/helpers.ts +33 -0
  68. package/src/hooks.ts +88 -0
  69. package/src/index.ts +3 -2
  70. package/src/instance.spec.ts +305 -489
  71. package/src/instance.ts +247 -391
  72. package/src/logger.spec.ts +212 -134
  73. package/src/logger.ts +36 -36
  74. package/src/murmurhash.ts +71 -0
  75. package/coverage/lcov-report/feature.ts.html +0 -508
  76. package/dist/bucket.d.ts +0 -30
  77. package/dist/feature.d.ts +0 -16
  78. package/dist/segments.d.ts +0 -5
  79. package/lib/bucket.d.ts +0 -30
  80. package/lib/feature.d.ts +0 -16
  81. package/lib/segments.d.ts +0 -5
  82. package/src/bucket.spec.ts +0 -37
  83. package/src/bucket.ts +0 -139
  84. package/src/feature.ts +0 -141
  85. package/src/segments.spec.ts +0 -468
  86. package/src/segments.ts +0 -58
package/src/evaluate.ts CHANGED
@@ -1,5 +1,4 @@
1
- import {
2
- Feature,
1
+ import type {
3
2
  FeatureKey,
4
3
  Context,
5
4
  BucketKey,
@@ -8,52 +7,53 @@ import {
8
7
  Traffic,
9
8
  Force,
10
9
  Required,
11
- OverrideFeature,
12
10
  Variation,
13
11
  VariationValue,
14
12
  VariableKey,
15
13
  VariableValue,
16
14
  VariableSchema,
15
+ EvaluatedFeature,
17
16
  StickyFeatures,
18
- InitialFeatures,
19
17
  Allocation,
20
18
  } from "@featurevisor/types";
21
19
 
22
20
  import { Logger } from "./logger";
21
+ import { HooksManager } from "./hooks";
23
22
  import { DatafileReader } from "./datafileReader";
24
- import { getBucket, ConfigureBucketKey, ConfigureBucketValue } from "./bucket";
25
- import {
26
- getMatchedTraffic,
27
- getMatchedTrafficAndAllocation,
28
- findForceFromFeature,
29
- parseFromStringifiedSegments,
30
- } from "./feature";
31
- import { allConditionsAreMatched } from "./conditions";
32
- import { allGroupSegmentsAreMatched } from "./segments";
33
- import type { Statuses, InterceptContext } from "./instance";
23
+ import { getBucketKey, getBucketedNumber } from "./bucketer";
34
24
 
35
25
  export enum EvaluationReason {
36
- NOT_FOUND = "not_found",
37
- NO_VARIATIONS = "no_variations",
38
- NO_MATCH = "no_match",
39
- DISABLED = "disabled",
40
- REQUIRED = "required",
41
- OUT_OF_RANGE = "out_of_range",
42
- FORCED = "forced",
43
- INITIAL = "initial",
44
- STICKY = "sticky",
45
- RULE = "rule",
46
- ALLOCATED = "allocated",
47
- DEFAULTED = "defaulted",
48
- OVERRIDE = "override",
49
- ERROR = "error",
26
+ // feature specific
27
+ FEATURE_NOT_FOUND = "feature_not_found", // feature is not found in datafile
28
+ DISABLED = "disabled", // feature is disabled
29
+ REQUIRED = "required", // required features are not enabled
30
+ OUT_OF_RANGE = "out_of_range", // out of range when mutually exclusive experiments are involved via Groups
31
+
32
+ // variations specific
33
+ NO_VARIATIONS = "no_variations", // feature has no variations
34
+ VARIATION_DISABLED = "variation_disabled", // feature is disabled, and variation's disabledVariationValue is used
35
+
36
+ // variable specific
37
+ VARIABLE_NOT_FOUND = "variable_not_found", // variable's schema is not defined in the feature
38
+ VARIABLE_DEFAULT = "variable_default", // default variable value used
39
+ VARIABLE_DISABLED = "variable_disabled", // feature is disabled, and variable's disabledValue is used
40
+ VARIABLE_OVERRIDE = "variable_override", // variable overridden from inside a variation
41
+
42
+ // common
43
+ NO_MATCH = "no_match", // no rules matched
44
+ FORCED = "forced", // against a forced rule
45
+ STICKY = "sticky", // against a sticky feature
46
+ RULE = "rule", // against a regular rule
47
+ ALLOCATED = "allocated", // regular allocation based on bucketing
48
+
49
+ ERROR = "error", // error
50
50
  }
51
51
 
52
52
  type EvaluationType = "flag" | "variation" | "variable";
53
53
 
54
54
  export interface Evaluation {
55
55
  // required
56
- // type: EvaluationType; // @TODO: bring in later
56
+ type: EvaluationType;
57
57
  featureKey: FeatureKey;
58
58
  reason: EvaluationReason;
59
59
 
@@ -67,8 +67,7 @@ export interface Evaluation {
67
67
  forceIndex?: number;
68
68
  force?: Force;
69
69
  required?: Required[];
70
- sticky?: OverrideFeature;
71
- initial?: OverrideFeature;
70
+ sticky?: EvaluatedFeature;
72
71
 
73
72
  // variation
74
73
  variation?: Variation;
@@ -80,47 +79,95 @@ export interface Evaluation {
80
79
  variableSchema?: VariableSchema;
81
80
  }
82
81
 
83
- export interface EvaluateOptions {
84
- type: EvaluationType;
85
-
86
- featureKey: FeatureKey | Feature;
87
- variableKey?: VariableKey;
82
+ export interface EvaluateDependencies {
88
83
  context: Context;
89
84
 
90
85
  logger: Logger;
86
+ hooksManager: HooksManager;
91
87
  datafileReader: DatafileReader;
92
- statuses?: Statuses;
93
- interceptContext?: InterceptContext;
94
88
 
95
- stickyFeatures?: StickyFeatures;
96
- initialFeatures?: InitialFeatures;
89
+ // OverrideOptions
90
+ sticky?: StickyFeatures;
91
+
92
+ defaultVariationValue?: VariationValue;
93
+ defaultVariableValue?: VariableValue;
94
+ }
95
+
96
+ export interface EvaluateParams {
97
+ type: EvaluationType;
98
+ featureKey: FeatureKey;
99
+ variableKey?: VariableKey;
100
+ }
101
+
102
+ export type EvaluateOptions = EvaluateParams & EvaluateDependencies;
103
+
104
+ export function evaluateWithHooks(opts: EvaluateOptions): Evaluation {
105
+ try {
106
+ const { hooksManager } = opts;
107
+ const hooks = hooksManager.getAll();
108
+
109
+ // run before hooks
110
+ let options = opts;
111
+ for (const hook of hooksManager.getAll()) {
112
+ if (hook.before) {
113
+ options = hook.before(options);
114
+ }
115
+ }
116
+
117
+ // evaluate
118
+ let evaluation = evaluate(options);
97
119
 
98
- bucketKeySeparator?: string;
99
- configureBucketKey?: ConfigureBucketKey;
100
- configureBucketValue?: ConfigureBucketValue;
120
+ // default: variation
121
+ if (
122
+ typeof options.defaultVariationValue !== "undefined" &&
123
+ evaluation.type === "variation" &&
124
+ typeof evaluation.variationValue === "undefined"
125
+ ) {
126
+ evaluation.variationValue = options.defaultVariationValue;
127
+ }
128
+
129
+ // default: variable
130
+ if (
131
+ typeof options.defaultVariableValue !== "undefined" &&
132
+ evaluation.type === "variable" &&
133
+ typeof evaluation.variableValue === "undefined"
134
+ ) {
135
+ evaluation.variableValue = options.defaultVariableValue;
136
+ }
137
+
138
+ // run after hooks
139
+ for (const hook of hooks) {
140
+ if (hook.after) {
141
+ evaluation = hook.after(evaluation, options);
142
+ }
143
+ }
144
+
145
+ return evaluation;
146
+ } catch (e) {
147
+ const { type, featureKey, variableKey, logger } = opts;
148
+
149
+ const evaluation: Evaluation = {
150
+ type,
151
+ featureKey,
152
+ variableKey,
153
+ reason: EvaluationReason.ERROR,
154
+ error: e,
155
+ };
156
+
157
+ logger.error("error during evaluation", evaluation);
158
+
159
+ return evaluation;
160
+ }
101
161
  }
102
162
 
103
163
  export function evaluate(options: EvaluateOptions): Evaluation {
164
+ const { type, featureKey, variableKey, context, logger, datafileReader, sticky, hooksManager } =
165
+ options;
166
+
167
+ const hooks = hooksManager.getAll();
104
168
  let evaluation: Evaluation;
105
- const {
106
- type,
107
- featureKey,
108
- variableKey,
109
- context,
110
- logger,
111
- datafileReader,
112
- statuses,
113
- stickyFeatures,
114
- initialFeatures,
115
- interceptContext,
116
- bucketKeySeparator,
117
- configureBucketKey,
118
- configureBucketValue,
119
- } = options;
120
169
 
121
170
  try {
122
- const key = typeof featureKey === "string" ? featureKey : featureKey.key;
123
-
124
171
  /**
125
172
  * Root flag evaluation
126
173
  */
@@ -128,25 +175,66 @@ export function evaluate(options: EvaluateOptions): Evaluation {
128
175
  if (type !== "flag") {
129
176
  // needed by variation and variable evaluations
130
177
  flag = evaluate({
178
+ ...options,
131
179
  type: "flag",
132
- featureKey: key,
133
- context,
134
- logger,
135
- datafileReader,
136
- statuses,
137
- stickyFeatures,
138
- initialFeatures,
139
- bucketKeySeparator,
140
- configureBucketKey,
141
- configureBucketValue,
142
180
  });
143
181
 
144
182
  if (flag.enabled === false) {
145
183
  evaluation = {
146
- featureKey: key,
184
+ type,
185
+ featureKey,
147
186
  reason: EvaluationReason.DISABLED,
148
187
  };
149
188
 
189
+ const feature = datafileReader.getFeature(featureKey);
190
+
191
+ // serve variable default value if feature is disabled (if explicitly specified)
192
+ if (type === "variable") {
193
+ if (
194
+ feature &&
195
+ variableKey &&
196
+ feature.variablesSchema &&
197
+ feature.variablesSchema[variableKey]
198
+ ) {
199
+ const variableSchema = feature.variablesSchema[variableKey];
200
+
201
+ if (typeof variableSchema.disabledValue !== "undefined") {
202
+ // disabledValue: <value>
203
+ evaluation = {
204
+ type,
205
+ featureKey,
206
+ reason: EvaluationReason.VARIABLE_DISABLED,
207
+ variableKey,
208
+ variableValue: variableSchema.disabledValue,
209
+ variableSchema,
210
+ enabled: false,
211
+ };
212
+ } else if (variableSchema.useDefaultWhenDisabled) {
213
+ // useDefaultWhenDisabled: true
214
+ evaluation = {
215
+ type,
216
+ featureKey,
217
+ reason: EvaluationReason.VARIABLE_DEFAULT,
218
+ variableKey,
219
+ variableValue: variableSchema.defaultValue,
220
+ variableSchema,
221
+ enabled: false,
222
+ };
223
+ }
224
+ }
225
+ }
226
+
227
+ // serve disabled variation value if feature is disabled (if explicitly specified)
228
+ if (type === "variation" && feature && feature.disabledVariationValue) {
229
+ evaluation = {
230
+ type,
231
+ featureKey,
232
+ reason: EvaluationReason.VARIATION_DISABLED,
233
+ variationValue: feature.disabledVariationValue,
234
+ enabled: false,
235
+ };
236
+ }
237
+
150
238
  logger.debug("feature is disabled", evaluation);
151
239
 
152
240
  return evaluation;
@@ -156,14 +244,15 @@ export function evaluate(options: EvaluateOptions): Evaluation {
156
244
  /**
157
245
  * Sticky
158
246
  */
159
- if (stickyFeatures && stickyFeatures[key]) {
247
+ if (sticky && sticky[featureKey]) {
160
248
  // flag
161
- if (type === "flag" && typeof stickyFeatures[key].enabled !== "undefined") {
249
+ if (type === "flag" && typeof sticky[featureKey].enabled !== "undefined") {
162
250
  evaluation = {
163
- featureKey: key,
251
+ type,
252
+ featureKey,
164
253
  reason: EvaluationReason.STICKY,
165
- sticky: stickyFeatures[key],
166
- enabled: stickyFeatures[key].enabled,
254
+ sticky: sticky[featureKey],
255
+ enabled: sticky[featureKey].enabled,
167
256
  };
168
257
 
169
258
  logger.debug("using sticky enabled", evaluation);
@@ -173,11 +262,12 @@ export function evaluate(options: EvaluateOptions): Evaluation {
173
262
 
174
263
  // variation
175
264
  if (type === "variation") {
176
- const variationValue = stickyFeatures[key].variation;
265
+ const variationValue = sticky[featureKey].variation;
177
266
 
178
267
  if (typeof variationValue !== "undefined") {
179
268
  evaluation = {
180
- featureKey: key,
269
+ type,
270
+ featureKey,
181
271
  reason: EvaluationReason.STICKY,
182
272
  variationValue,
183
273
  };
@@ -190,14 +280,15 @@ export function evaluate(options: EvaluateOptions): Evaluation {
190
280
 
191
281
  // variable
192
282
  if (variableKey) {
193
- const variables = stickyFeatures[key].variables;
283
+ const variables = sticky[featureKey].variables;
194
284
 
195
285
  if (variables) {
196
286
  const result = variables[variableKey];
197
287
 
198
288
  if (typeof result !== "undefined") {
199
289
  evaluation = {
200
- featureKey: key,
290
+ type,
291
+ featureKey,
201
292
  reason: EvaluationReason.STICKY,
202
293
  variableKey,
203
294
  variableValue: result,
@@ -211,60 +302,6 @@ export function evaluate(options: EvaluateOptions): Evaluation {
211
302
  }
212
303
  }
213
304
 
214
- /**
215
- * Initial
216
- */
217
- if (statuses && !statuses.ready && initialFeatures && initialFeatures[key]) {
218
- // flag
219
- if (type === "flag" && typeof initialFeatures[key].enabled !== "undefined") {
220
- evaluation = {
221
- featureKey: key,
222
- reason: EvaluationReason.INITIAL,
223
- initial: initialFeatures[key],
224
- enabled: initialFeatures[key].enabled,
225
- };
226
-
227
- logger.debug("using initial enabled", evaluation);
228
-
229
- return evaluation;
230
- }
231
-
232
- // variation
233
- if (type === "variation" && typeof initialFeatures[key].variation !== "undefined") {
234
- const variationValue = initialFeatures[key].variation;
235
-
236
- evaluation = {
237
- featureKey: key,
238
- reason: EvaluationReason.INITIAL,
239
- variationValue,
240
- };
241
-
242
- logger.debug("using initial variation", evaluation);
243
-
244
- return evaluation;
245
- }
246
-
247
- // variable
248
- if (variableKey) {
249
- const variables = initialFeatures[key].variables;
250
-
251
- if (variables) {
252
- if (typeof variables[variableKey] !== "undefined") {
253
- evaluation = {
254
- featureKey: key,
255
- reason: EvaluationReason.INITIAL,
256
- variableKey,
257
- variableValue: variables[variableKey],
258
- };
259
-
260
- logger.debug("using initial variable", evaluation);
261
-
262
- return evaluation;
263
- }
264
- }
265
- }
266
- }
267
-
268
305
  /**
269
306
  * Feature
270
307
  */
@@ -274,8 +311,9 @@ export function evaluate(options: EvaluateOptions): Evaluation {
274
311
  // feature: not found
275
312
  if (!feature) {
276
313
  evaluation = {
277
- featureKey: key,
278
- reason: EvaluationReason.NOT_FOUND, // @TODO: make it type-specific
314
+ type,
315
+ featureKey,
316
+ reason: EvaluationReason.FEATURE_NOT_FOUND,
279
317
  };
280
318
 
281
319
  logger.warn("feature not found", evaluation);
@@ -285,7 +323,7 @@ export function evaluate(options: EvaluateOptions): Evaluation {
285
323
 
286
324
  // feature: deprecated
287
325
  if (type === "flag" && feature.deprecated) {
288
- logger.warn("feature is deprecated", { featureKey: feature.key });
326
+ logger.warn("feature is deprecated", { featureKey });
289
327
  }
290
328
 
291
329
  // variableSchema
@@ -299,8 +337,9 @@ export function evaluate(options: EvaluateOptions): Evaluation {
299
337
  // variable schema not found
300
338
  if (!variableSchema) {
301
339
  evaluation = {
302
- featureKey: key,
303
- reason: EvaluationReason.NOT_FOUND,
340
+ type,
341
+ featureKey,
342
+ reason: EvaluationReason.VARIABLE_NOT_FOUND,
304
343
  variableKey,
305
344
  };
306
345
 
@@ -311,7 +350,7 @@ export function evaluate(options: EvaluateOptions): Evaluation {
311
350
 
312
351
  if (variableSchema.deprecated) {
313
352
  logger.warn("variable is deprecated", {
314
- featureKey: feature.key,
353
+ featureKey,
315
354
  variableKey,
316
355
  });
317
356
  }
@@ -320,7 +359,8 @@ export function evaluate(options: EvaluateOptions): Evaluation {
320
359
  // variation: no variations
321
360
  if (type === "variation" && (!feature.variations || feature.variations.length === 0)) {
322
361
  evaluation = {
323
- featureKey: key,
362
+ type,
363
+ featureKey,
324
364
  reason: EvaluationReason.NO_VARIATIONS,
325
365
  };
326
366
 
@@ -329,18 +369,17 @@ export function evaluate(options: EvaluateOptions): Evaluation {
329
369
  return evaluation;
330
370
  }
331
371
 
332
- const finalContext = interceptContext ? interceptContext(context) : context;
333
-
334
372
  /**
335
373
  * Forced
336
374
  */
337
- const { force, forceIndex } = findForceFromFeature(feature, context, datafileReader, logger);
375
+ const { force, forceIndex } = datafileReader.getMatchedForce(feature, context);
338
376
 
339
377
  if (force) {
340
378
  // flag
341
379
  if (type === "flag" && typeof force.enabled !== "undefined") {
342
380
  evaluation = {
343
- featureKey: feature.key,
381
+ type,
382
+ featureKey,
344
383
  reason: EvaluationReason.FORCED,
345
384
  forceIndex,
346
385
  force,
@@ -358,7 +397,8 @@ export function evaluate(options: EvaluateOptions): Evaluation {
358
397
 
359
398
  if (variation) {
360
399
  evaluation = {
361
- featureKey: feature.key,
400
+ type,
401
+ featureKey,
362
402
  reason: EvaluationReason.FORCED,
363
403
  forceIndex,
364
404
  force,
@@ -374,7 +414,8 @@ export function evaluate(options: EvaluateOptions): Evaluation {
374
414
  // variable
375
415
  if (variableKey && force.variables && typeof force.variables[variableKey] !== "undefined") {
376
416
  evaluation = {
377
- featureKey: feature.key,
417
+ type,
418
+ featureKey,
378
419
  reason: EvaluationReason.FORCED,
379
420
  forceIndex,
380
421
  force,
@@ -405,17 +446,9 @@ export function evaluate(options: EvaluateOptions): Evaluation {
405
446
  }
406
447
 
407
448
  const requiredEvaluation = evaluate({
449
+ ...options,
408
450
  type: "flag",
409
451
  featureKey: requiredKey,
410
- context: finalContext,
411
- logger,
412
- datafileReader,
413
- statuses,
414
- stickyFeatures,
415
- initialFeatures,
416
- bucketKeySeparator,
417
- configureBucketKey,
418
- configureBucketValue,
419
452
  });
420
453
  const requiredIsEnabled = requiredEvaluation.enabled;
421
454
 
@@ -425,17 +458,9 @@ export function evaluate(options: EvaluateOptions): Evaluation {
425
458
 
426
459
  if (typeof requiredVariation !== "undefined") {
427
460
  const requiredVariationEvaluation = evaluate({
461
+ ...options,
428
462
  type: "variation",
429
463
  featureKey: requiredKey,
430
- context: finalContext,
431
- logger,
432
- datafileReader,
433
- statuses,
434
- stickyFeatures,
435
- initialFeatures,
436
- bucketKeySeparator,
437
- configureBucketKey,
438
- configureBucketValue,
439
464
  });
440
465
 
441
466
  let requiredVariationValue;
@@ -454,7 +479,8 @@ export function evaluate(options: EvaluateOptions): Evaluation {
454
479
 
455
480
  if (!requiredFeaturesAreEnabled) {
456
481
  evaluation = {
457
- featureKey: feature.key,
482
+ type,
483
+ featureKey,
458
484
  reason: EvaluationReason.REQUIRED,
459
485
  required: feature.required,
460
486
  enabled: requiredFeaturesAreEnabled,
@@ -469,34 +495,71 @@ export function evaluate(options: EvaluateOptions): Evaluation {
469
495
  /**
470
496
  * Bucketing
471
497
  */
472
- const { bucketKey, bucketValue } = getBucket({
473
- feature,
474
- context: finalContext,
498
+ // bucketKey
499
+ let bucketKey = getBucketKey({
500
+ featureKey,
501
+ bucketBy: feature.bucketBy,
502
+ context,
503
+
475
504
  logger,
476
- bucketKeySeparator,
477
- configureBucketKey,
478
- configureBucketValue,
479
505
  });
506
+ for (const hook of hooks) {
507
+ if (hook.bucketKey) {
508
+ bucketKey = hook.bucketKey({
509
+ featureKey,
510
+ context,
511
+ bucketBy: feature.bucketBy,
512
+ bucketKey,
513
+ });
514
+ }
515
+ }
516
+
517
+ // bucketValue
518
+ let bucketValue = getBucketedNumber(bucketKey);
519
+
520
+ for (const hook of hooks) {
521
+ if (hook.bucketValue) {
522
+ bucketValue = hook.bucketValue({
523
+ featureKey,
524
+ bucketKey,
525
+ context,
526
+ bucketValue,
527
+ });
528
+ }
529
+ }
480
530
 
481
531
  let matchedTraffic: Traffic | undefined;
482
532
  let matchedAllocation: Allocation | undefined;
483
533
 
484
534
  if (type !== "flag") {
485
- const matched = getMatchedTrafficAndAllocation(
486
- feature.traffic,
487
- finalContext,
488
- bucketValue,
489
- datafileReader,
490
- logger,
491
- );
535
+ matchedTraffic = datafileReader.getMatchedTraffic(feature.traffic, context);
492
536
 
493
- matchedTraffic = matched.matchedTraffic;
494
- matchedAllocation = matched.matchedAllocation;
537
+ if (matchedTraffic) {
538
+ matchedAllocation = datafileReader.getMatchedAllocation(matchedTraffic, bucketValue);
539
+ }
495
540
  } else {
496
- matchedTraffic = getMatchedTraffic(feature.traffic, finalContext, datafileReader, logger);
541
+ matchedTraffic = datafileReader.getMatchedTraffic(feature.traffic, context);
497
542
  }
498
543
 
499
544
  if (matchedTraffic) {
545
+ // percentage: 0
546
+ if (matchedTraffic.percentage === 0) {
547
+ evaluation = {
548
+ type,
549
+ featureKey,
550
+ reason: EvaluationReason.RULE,
551
+ bucketKey,
552
+ bucketValue,
553
+ ruleKey: matchedTraffic.key,
554
+ traffic: matchedTraffic,
555
+ enabled: false,
556
+ };
557
+
558
+ logger.debug("matched rule with 0 percentage", evaluation);
559
+
560
+ return evaluation;
561
+ }
562
+
500
563
  // flag
501
564
  if (type === "flag") {
502
565
  // flag: check if mutually exclusive
@@ -508,7 +571,8 @@ export function evaluate(options: EvaluateOptions): Evaluation {
508
571
  // matched
509
572
  if (matchedRange) {
510
573
  evaluation = {
511
- featureKey: feature.key,
574
+ type,
575
+ featureKey,
512
576
  reason: EvaluationReason.ALLOCATED,
513
577
  bucketKey,
514
578
  bucketValue,
@@ -525,7 +589,8 @@ export function evaluate(options: EvaluateOptions): Evaluation {
525
589
 
526
590
  // no match
527
591
  evaluation = {
528
- featureKey: feature.key,
592
+ type,
593
+ featureKey,
529
594
  reason: EvaluationReason.OUT_OF_RANGE,
530
595
  bucketKey,
531
596
  bucketValue,
@@ -540,8 +605,9 @@ export function evaluate(options: EvaluateOptions): Evaluation {
540
605
  // flag: override from rule
541
606
  if (typeof matchedTraffic.enabled !== "undefined") {
542
607
  evaluation = {
543
- featureKey: feature.key,
544
- reason: EvaluationReason.OVERRIDE,
608
+ type,
609
+ featureKey,
610
+ reason: EvaluationReason.RULE,
545
611
  bucketKey,
546
612
  bucketValue,
547
613
  ruleKey: matchedTraffic.key,
@@ -557,7 +623,8 @@ export function evaluate(options: EvaluateOptions): Evaluation {
557
623
  // treated as enabled because of matched traffic
558
624
  if (bucketValue <= matchedTraffic.percentage) {
559
625
  evaluation = {
560
- featureKey: feature.key,
626
+ type,
627
+ featureKey,
561
628
  reason: EvaluationReason.RULE,
562
629
  bucketKey,
563
630
  bucketValue,
@@ -580,7 +647,8 @@ export function evaluate(options: EvaluateOptions): Evaluation {
580
647
 
581
648
  if (variation) {
582
649
  evaluation = {
583
- featureKey: feature.key,
650
+ type,
651
+ featureKey,
584
652
  reason: EvaluationReason.RULE,
585
653
  bucketKey,
586
654
  bucketValue,
@@ -601,7 +669,8 @@ export function evaluate(options: EvaluateOptions): Evaluation {
601
669
 
602
670
  if (variation) {
603
671
  evaluation = {
604
- featureKey: feature.key,
672
+ type,
673
+ featureKey,
605
674
  reason: EvaluationReason.ALLOCATED,
606
675
  bucketKey,
607
676
  bucketValue,
@@ -627,7 +696,8 @@ export function evaluate(options: EvaluateOptions): Evaluation {
627
696
  typeof matchedTraffic.variables[variableKey] !== "undefined"
628
697
  ) {
629
698
  evaluation = {
630
- featureKey: feature.key,
699
+ type,
700
+ featureKey,
631
701
  reason: EvaluationReason.RULE,
632
702
  bucketKey,
633
703
  bucketValue,
@@ -657,70 +727,71 @@ export function evaluate(options: EvaluateOptions): Evaluation {
657
727
  if (variationValue && Array.isArray(feature.variations)) {
658
728
  const variation = feature.variations.find((v) => v.value === variationValue);
659
729
 
660
- if (variation && variation.variables) {
661
- const variableFromVariation = variation.variables.find((v) => v.key === variableKey);
662
-
663
- if (variableFromVariation) {
664
- if (variableFromVariation.overrides) {
665
- const override = variableFromVariation.overrides.find((o) => {
666
- if (o.conditions) {
667
- return allConditionsAreMatched(
668
- typeof o.conditions === "string" ? JSON.parse(o.conditions) : o.conditions,
669
- finalContext,
670
- logger,
671
- );
672
- }
673
-
674
- if (o.segments) {
675
- return allGroupSegmentsAreMatched(
676
- parseFromStringifiedSegments(o.segments),
677
- finalContext,
678
- datafileReader,
679
- logger,
680
- );
681
- }
682
-
683
- return false;
684
- });
685
-
686
- if (override) {
687
- evaluation = {
688
- featureKey: feature.key,
689
- reason: EvaluationReason.OVERRIDE,
690
- bucketKey,
691
- bucketValue,
692
- ruleKey: matchedTraffic?.key,
693
- traffic: matchedTraffic,
694
- variableKey,
695
- variableSchema,
696
- variableValue: override.value,
697
- };
698
-
699
- logger.debug("variable override", evaluation);
700
-
701
- return evaluation;
702
- }
730
+ if (variation && variation.variableOverrides && variation.variableOverrides[variableKey]) {
731
+ const overrides = variation.variableOverrides[variableKey];
732
+
733
+ const override = overrides.find((o) => {
734
+ if (o.conditions) {
735
+ return datafileReader.allConditionsAreMatched(
736
+ typeof o.conditions === "string" && o.conditions !== "*"
737
+ ? JSON.parse(o.conditions)
738
+ : o.conditions,
739
+ context,
740
+ );
703
741
  }
704
742
 
705
- if (typeof variableFromVariation.value !== "undefined") {
706
- evaluation = {
707
- featureKey: feature.key,
708
- reason: EvaluationReason.ALLOCATED,
709
- bucketKey,
710
- bucketValue,
711
- ruleKey: matchedTraffic?.key,
712
- traffic: matchedTraffic,
713
- variableKey,
714
- variableSchema,
715
- variableValue: variableFromVariation.value,
716
- };
743
+ if (o.segments) {
744
+ return datafileReader.allSegmentsAreMatched(
745
+ datafileReader.parseSegmentsIfStringified(o.segments),
746
+ context,
747
+ );
748
+ }
717
749
 
718
- logger.debug("allocated variable", evaluation);
750
+ return false;
751
+ });
719
752
 
720
- return evaluation;
721
- }
753
+ if (override) {
754
+ evaluation = {
755
+ type,
756
+ featureKey,
757
+ reason: EvaluationReason.VARIABLE_OVERRIDE,
758
+ bucketKey,
759
+ bucketValue,
760
+ ruleKey: matchedTraffic?.key,
761
+ traffic: matchedTraffic,
762
+ variableKey,
763
+ variableSchema,
764
+ variableValue: override.value,
765
+ };
766
+
767
+ logger.debug("variable override", evaluation);
768
+
769
+ return evaluation;
722
770
  }
723
771
  }
772
+
773
+ if (
774
+ variation &&
775
+ variation.variables &&
776
+ typeof variation.variables[variableKey] !== "undefined"
777
+ ) {
778
+ evaluation = {
779
+ type,
780
+ featureKey,
781
+ reason: EvaluationReason.ALLOCATED,
782
+ bucketKey,
783
+ bucketValue,
784
+ ruleKey: matchedTraffic?.key,
785
+ traffic: matchedTraffic,
786
+ variableKey,
787
+ variableSchema,
788
+ variableValue: variation.variables[variableKey],
789
+ };
790
+
791
+ logger.debug("allocated variable", evaluation);
792
+
793
+ return evaluation;
794
+ }
724
795
  }
725
796
  }
726
797
 
@@ -729,7 +800,8 @@ export function evaluate(options: EvaluateOptions): Evaluation {
729
800
  */
730
801
  if (type === "variation") {
731
802
  evaluation = {
732
- featureKey: feature.key,
803
+ type,
804
+ featureKey,
733
805
  reason: EvaluationReason.NO_MATCH,
734
806
  bucketKey,
735
807
  bucketValue,
@@ -740,24 +812,41 @@ export function evaluate(options: EvaluateOptions): Evaluation {
740
812
  return evaluation;
741
813
  }
742
814
 
743
- if (type === "variable" && variableSchema) {
815
+ if (type === "variable") {
816
+ if (variableSchema) {
817
+ evaluation = {
818
+ type,
819
+ featureKey,
820
+ reason: EvaluationReason.VARIABLE_DEFAULT,
821
+ bucketKey,
822
+ bucketValue,
823
+ variableKey,
824
+ variableSchema,
825
+ variableValue: variableSchema.defaultValue,
826
+ };
827
+
828
+ logger.debug("using default value", evaluation);
829
+
830
+ return evaluation;
831
+ }
832
+
744
833
  evaluation = {
745
- featureKey: feature.key,
746
- reason: EvaluationReason.DEFAULTED,
834
+ type,
835
+ featureKey,
836
+ reason: EvaluationReason.VARIABLE_NOT_FOUND,
837
+ variableKey,
747
838
  bucketKey,
748
839
  bucketValue,
749
- variableKey,
750
- variableSchema,
751
- variableValue: variableSchema.defaultValue,
752
840
  };
753
841
 
754
- logger.debug("using default value", evaluation);
842
+ logger.debug("variable not found", evaluation);
755
843
 
756
844
  return evaluation;
757
845
  }
758
846
 
759
847
  evaluation = {
760
- featureKey: feature.key,
848
+ type,
849
+ featureKey,
761
850
  reason: EvaluationReason.NO_MATCH,
762
851
  bucketKey,
763
852
  bucketValue,
@@ -769,7 +858,9 @@ export function evaluate(options: EvaluateOptions): Evaluation {
769
858
  return evaluation;
770
859
  } catch (e) {
771
860
  evaluation = {
772
- featureKey: typeof featureKey === "string" ? featureKey : featureKey.key,
861
+ type,
862
+ featureKey,
863
+ variableKey,
773
864
  reason: EvaluationReason.ERROR,
774
865
  error: e,
775
866
  };