@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/instance.ts CHANGED
@@ -1,359 +1,228 @@
1
- import {
1
+ import type {
2
2
  Context,
3
- DatafileContent,
4
3
  Feature,
5
4
  FeatureKey,
6
- InitialFeatures,
7
5
  StickyFeatures,
8
- VariableType,
6
+ EvaluatedFeatures,
7
+ EvaluatedFeature,
9
8
  VariableValue,
10
9
  VariationValue,
11
10
  VariableKey,
11
+ DatafileContent,
12
12
  } from "@featurevisor/types";
13
13
 
14
14
  import { createLogger, Logger, LogLevel } from "./logger";
15
+ import { HooksManager, Hook } from "./hooks";
16
+ import { Emitter, EventCallback, EventName } from "./emitter";
15
17
  import { DatafileReader } from "./datafileReader";
16
- import { Emitter } from "./emitter";
17
- import { ConfigureBucketKey, ConfigureBucketValue } from "./bucket";
18
- import { Evaluation, evaluate } from "./evaluate";
19
-
20
- export type ReadyCallback = () => void;
21
-
22
- export type ActivationCallback = (
23
- featureName: string,
24
- variation: VariationValue,
25
- context: Context,
26
- captureContext: Context,
27
- ) => void;
28
-
29
- export interface Statuses {
30
- ready: boolean;
31
- refreshInProgress: boolean;
32
- }
33
-
34
- const DEFAULT_BUCKET_KEY_SEPARATOR = ".";
35
-
36
- export type InterceptContext = (context: Context) => Context;
37
-
38
- export interface InstanceOptions {
39
- bucketKeySeparator?: string;
40
- configureBucketKey?: ConfigureBucketKey;
41
- configureBucketValue?: ConfigureBucketValue;
42
- datafile?: DatafileContent | string;
43
- datafileUrl?: string;
44
- handleDatafileFetch?: (datafileUrl: string) => Promise<DatafileContent>;
45
- initialFeatures?: InitialFeatures;
46
- interceptContext?: InterceptContext;
47
- logger?: Logger;
48
- onActivation?: ActivationCallback;
49
- onReady?: ReadyCallback;
50
- onRefresh?: () => void;
51
- onUpdate?: () => void;
52
- refreshInterval?: number; // seconds
53
- stickyFeatures?: StickyFeatures;
54
- }
18
+ import { Evaluation, EvaluateDependencies, evaluateWithHooks } from "./evaluate";
19
+ import { FeaturevisorChildInstance } from "./child";
20
+ import { getParamsForStickySetEvent, getParamsForDatafileSetEvent } from "./events";
21
+ import { getValueByType } from "./helpers";
55
22
 
56
23
  const emptyDatafile: DatafileContent = {
57
- schemaVersion: "1",
24
+ schemaVersion: "2",
58
25
  revision: "unknown",
59
- attributes: [],
60
- segments: [],
61
- features: [],
26
+ segments: {},
27
+ features: {},
62
28
  };
63
29
 
64
- export type DatafileFetchHandler = (datafileUrl: string) => Promise<DatafileContent>;
65
-
66
- function fetchDatafileContent(
67
- datafileUrl,
68
- handleDatafileFetch?: DatafileFetchHandler,
69
- ): Promise<DatafileContent> {
70
- if (handleDatafileFetch) {
71
- return handleDatafileFetch(datafileUrl);
72
- }
30
+ export interface OverrideOptions {
31
+ sticky?: StickyFeatures;
73
32
 
74
- return fetch(datafileUrl).then((res) => res.json());
33
+ defaultVariationValue?: VariationValue;
34
+ defaultVariableValue?: VariableValue;
75
35
  }
76
36
 
77
- type FieldType = string | VariableType;
78
- type ValueType = VariableValue;
79
-
80
- export function getValueByType(value: ValueType, fieldType: FieldType): ValueType {
81
- try {
82
- if (value === undefined) {
83
- return undefined;
84
- }
85
-
86
- switch (fieldType) {
87
- case "string":
88
- return typeof value === "string" ? value : undefined;
89
- case "integer":
90
- return parseInt(value as string, 10);
91
- case "double":
92
- return parseFloat(value as string);
93
- case "boolean":
94
- return value === true;
95
- case "array":
96
- return Array.isArray(value) ? value : undefined;
97
- case "object":
98
- return typeof value === "object" ? value : undefined;
99
- // @NOTE: `json` is not handled here intentionally
100
- default:
101
- return value;
102
- }
103
- } catch (e) {
104
- return undefined;
105
- }
37
+ export interface InstanceOptions {
38
+ datafile?: DatafileContent | string;
39
+ context?: Context;
40
+ logLevel?: LogLevel;
41
+ logger?: Logger;
42
+ sticky?: StickyFeatures;
43
+ hooks?: Hook[];
106
44
  }
107
45
 
108
46
  export class FeaturevisorInstance {
109
47
  // from options
110
- private bucketKeySeparator: string;
111
- private configureBucketKey?: ConfigureBucketKey;
112
- private configureBucketValue?: ConfigureBucketValue;
113
- private datafileUrl?: string;
114
- private handleDatafileFetch?: DatafileFetchHandler;
115
- private initialFeatures?: InitialFeatures;
116
- private interceptContext?: InterceptContext;
48
+ private context: Context = {};
117
49
  private logger: Logger;
118
- private refreshInterval?: number; // seconds
119
- private stickyFeatures?: StickyFeatures;
50
+ private sticky?: StickyFeatures;
120
51
 
121
52
  // internally created
122
53
  private datafileReader: DatafileReader;
54
+ private hooksManager: HooksManager;
123
55
  private emitter: Emitter;
124
- private statuses: Statuses;
125
- private intervalId?: ReturnType<typeof setInterval>;
126
-
127
- // exposed from emitter
128
- public on: Emitter["addListener"];
129
- public addListener: Emitter["addListener"];
130
- public off: Emitter["removeListener"];
131
- public removeListener: Emitter["removeListener"];
132
- public removeAllListeners: Emitter["removeAllListeners"];
133
56
 
134
57
  constructor(options: InstanceOptions) {
135
58
  // from options
136
- this.bucketKeySeparator = options.bucketKeySeparator || DEFAULT_BUCKET_KEY_SEPARATOR;
137
- this.configureBucketKey = options.configureBucketKey;
138
- this.configureBucketValue = options.configureBucketValue;
139
- this.datafileUrl = options.datafileUrl;
140
- this.handleDatafileFetch = options.handleDatafileFetch;
141
- this.initialFeatures = options.initialFeatures;
142
- this.interceptContext = options.interceptContext;
143
- this.logger = options.logger || createLogger();
144
- this.refreshInterval = options.refreshInterval;
145
- this.stickyFeatures = options.stickyFeatures;
146
-
147
- // internal
59
+ this.context = options.context || {};
60
+ this.logger =
61
+ options.logger ||
62
+ createLogger({
63
+ level: options.logLevel || Logger.defaultLevel,
64
+ });
65
+ this.hooksManager = new HooksManager({
66
+ hooks: options.hooks || [],
67
+ logger: this.logger,
68
+ });
148
69
  this.emitter = new Emitter();
149
- this.statuses = {
150
- ready: false,
151
- refreshInProgress: false,
152
- };
153
-
154
- // register events
155
- if (options.onReady) {
156
- this.emitter.addListener("ready", options.onReady);
157
- }
70
+ this.sticky = options.sticky;
158
71
 
159
- if (options.onRefresh) {
160
- this.emitter.addListener("refresh", options.onRefresh);
161
- }
162
-
163
- if (options.onUpdate) {
164
- this.emitter.addListener("update", options.onUpdate);
165
- }
166
-
167
- if (options.onActivation) {
168
- this.emitter.addListener("activation", options.onActivation);
72
+ // datafile
73
+ this.datafileReader = new DatafileReader({
74
+ datafile: emptyDatafile,
75
+ logger: this.logger,
76
+ });
77
+ if (options.datafile) {
78
+ this.datafileReader = new DatafileReader({
79
+ datafile:
80
+ typeof options.datafile === "string" ? JSON.parse(options.datafile) : options.datafile,
81
+ logger: this.logger,
82
+ });
169
83
  }
170
84
 
171
- // expose emitter methods
172
- const on = this.emitter.addListener.bind(this.emitter);
173
- this.on = on;
174
- this.addListener = on;
175
-
176
- const off = this.emitter.removeListener.bind(this.emitter);
177
- this.off = off;
178
- this.removeListener = off;
85
+ this.logger.info("Featurevisor SDK initialized");
86
+ }
179
87
 
180
- this.removeAllListeners = this.emitter.removeAllListeners.bind(this.emitter);
88
+ setLogLevel(level: LogLevel) {
89
+ this.logger.setLevel(level);
90
+ }
181
91
 
182
- // datafile
183
- if (options.datafileUrl) {
184
- this.setDatafile(options.datafile || emptyDatafile);
92
+ setDatafile(datafile: DatafileContent | string) {
93
+ try {
94
+ const newDatafileReader = new DatafileReader({
95
+ datafile: typeof datafile === "string" ? JSON.parse(datafile) : datafile,
96
+ logger: this.logger,
97
+ });
185
98
 
186
- fetchDatafileContent(options.datafileUrl, options.handleDatafileFetch)
187
- .then((datafile) => {
188
- this.setDatafile(datafile);
99
+ const details = getParamsForDatafileSetEvent(this.datafileReader, newDatafileReader);
189
100
 
190
- this.statuses.ready = true;
191
- this.emitter.emit("ready");
101
+ this.datafileReader = newDatafileReader;
192
102
 
193
- if (this.refreshInterval) {
194
- this.startRefreshing();
195
- }
196
- })
197
- .catch((e) => {
198
- this.logger.error("failed to fetch datafile", { error: e });
199
- });
200
- } else if (options.datafile) {
201
- this.setDatafile(options.datafile);
202
- this.statuses.ready = true;
203
-
204
- setTimeout(() => {
205
- this.emitter.emit("ready");
206
- }, 0);
207
- } else {
208
- throw new Error(
209
- "Featurevisor SDK instance cannot be created without both `datafile` and `datafileUrl` options",
210
- );
103
+ this.logger.info("datafile set", details);
104
+ this.emitter.trigger("datafile_set", details);
105
+ } catch (e) {
106
+ this.logger.error("could not parse datafile", { error: e });
211
107
  }
212
108
  }
213
109
 
214
- setLogLevels(levels: LogLevel[]) {
215
- this.logger.setLevels(levels);
216
- }
217
-
218
- onReady(): Promise<FeaturevisorInstance> {
219
- return new Promise((resolve) => {
220
- if (this.statuses.ready) {
221
- return resolve(this);
222
- }
223
-
224
- const cb = () => {
225
- this.emitter.removeListener("ready", cb);
110
+ setSticky(sticky: StickyFeatures, replace = false) {
111
+ const previousStickyFeatures = this.sticky || {};
226
112
 
227
- resolve(this);
113
+ if (replace) {
114
+ this.sticky = { ...sticky };
115
+ } else {
116
+ this.sticky = {
117
+ ...this.sticky,
118
+ ...sticky,
228
119
  };
120
+ }
229
121
 
230
- this.emitter.addListener("ready", cb);
231
- });
122
+ const params = getParamsForStickySetEvent(previousStickyFeatures, this.sticky, replace);
123
+
124
+ this.logger.info("sticky features set", params);
125
+ this.emitter.trigger("sticky_set", params);
232
126
  }
233
127
 
234
- setDatafile(datafile: DatafileContent | string) {
235
- try {
236
- this.datafileReader = new DatafileReader(
237
- typeof datafile === "string" ? JSON.parse(datafile) : datafile,
238
- );
239
- } catch (e) {
240
- this.logger.error("could not parse datafile", { error: e });
241
- }
128
+ getRevision(): string {
129
+ return this.datafileReader.getRevision();
242
130
  }
243
131
 
244
- setStickyFeatures(stickyFeatures: StickyFeatures | undefined) {
245
- this.stickyFeatures = stickyFeatures;
132
+ getFeature(featureKey: string): Feature | undefined {
133
+ return this.datafileReader.getFeature(featureKey);
246
134
  }
247
135
 
248
- getRevision(): string {
249
- return this.datafileReader.getRevision();
136
+ addHook(hook: Hook) {
137
+ return this.hooksManager.add(hook);
250
138
  }
251
139
 
252
- getFeature(featureKey: string | Feature): Feature | undefined {
253
- return typeof featureKey === "string"
254
- ? this.datafileReader.getFeature(featureKey) // only key provided
255
- : featureKey; // full feature provided
140
+ on(eventName: EventName, callback: EventCallback) {
141
+ return this.emitter.on(eventName, callback);
256
142
  }
257
143
 
258
- /**
259
- * Statuses
260
- */
261
- isReady(): boolean {
262
- return this.statuses.ready;
144
+ close() {
145
+ this.emitter.clearAll();
263
146
  }
264
147
 
265
148
  /**
266
- * Refresh
149
+ * Context
267
150
  */
268
- refresh() {
269
- this.logger.debug("refreshing datafile");
270
-
271
- if (this.statuses.refreshInProgress) {
272
- return this.logger.warn("refresh in progress, skipping");
273
- }
274
-
275
- if (!this.datafileUrl) {
276
- return this.logger.error("cannot refresh since `datafileUrl` is not provided");
151
+ setContext(context: Context, replace = false) {
152
+ if (replace) {
153
+ this.context = context;
154
+ } else {
155
+ this.context = { ...this.context, ...context };
277
156
  }
278
157
 
279
- this.statuses.refreshInProgress = true;
280
-
281
- fetchDatafileContent(this.datafileUrl, this.handleDatafileFetch)
282
- .then((datafile) => {
283
- const currentRevision = this.getRevision();
284
- const newRevision = datafile.revision;
285
- const isNotSameRevision = currentRevision !== newRevision;
286
-
287
- this.setDatafile(datafile);
288
- this.logger.info("refreshed datafile");
289
-
290
- this.emitter.emit("refresh");
291
-
292
- if (isNotSameRevision) {
293
- this.emitter.emit("update");
294
- }
295
-
296
- this.statuses.refreshInProgress = false;
297
- })
298
- .catch((e) => {
299
- this.logger.error("failed to refresh datafile", { error: e });
300
- this.statuses.refreshInProgress = false;
301
- });
158
+ this.emitter.trigger("context_set", {
159
+ context: this.context,
160
+ replaced: replace,
161
+ });
162
+ this.logger.debug(replace ? "context replaced" : "context updated", {
163
+ context: this.context,
164
+ replaced: replace,
165
+ });
302
166
  }
303
167
 
304
- startRefreshing() {
305
- if (!this.datafileUrl) {
306
- return this.logger.error("cannot start refreshing since `datafileUrl` is not provided");
307
- }
308
-
309
- if (this.intervalId) {
310
- return this.logger.warn("refreshing has already started");
311
- }
312
-
313
- if (!this.refreshInterval) {
314
- return this.logger.warn("no `refreshInterval` option provided");
315
- }
316
-
317
- this.intervalId = setInterval(() => {
318
- this.refresh();
319
- }, this.refreshInterval * 1000);
168
+ getContext(context?: Context): Context {
169
+ return context
170
+ ? {
171
+ ...this.context,
172
+ ...context,
173
+ }
174
+ : this.context;
320
175
  }
321
176
 
322
- stopRefreshing() {
323
- if (!this.intervalId) {
324
- return this.logger.warn("refreshing has not started yet");
325
- }
326
-
327
- clearInterval(this.intervalId);
177
+ spawn(context: Context = {}, options: OverrideOptions = {}): FeaturevisorChildInstance {
178
+ return new FeaturevisorChildInstance({
179
+ parent: this,
180
+ context: this.getContext(context),
181
+ sticky: options.sticky,
182
+ });
328
183
  }
329
184
 
330
185
  /**
331
186
  * Flag
332
187
  */
333
- evaluateFlag(featureKey: FeatureKey | Feature, context: Context = {}): Evaluation {
334
- return evaluate({
335
- type: "flag",
336
-
337
- featureKey,
338
- context,
188
+ private getEvaluationDependencies(
189
+ context: Context,
190
+ options: OverrideOptions = {},
191
+ ): EvaluateDependencies {
192
+ return {
193
+ context: this.getContext(context),
339
194
 
340
195
  logger: this.logger,
196
+ hooksManager: this.hooksManager,
341
197
  datafileReader: this.datafileReader,
342
- statuses: this.statuses,
343
- interceptContext: this.interceptContext,
344
198
 
345
- stickyFeatures: this.stickyFeatures,
346
- initialFeatures: this.initialFeatures,
199
+ // OverrideOptions
200
+ sticky: options.sticky
201
+ ? {
202
+ ...this.sticky,
203
+ ...options.sticky,
204
+ }
205
+ : this.sticky,
206
+ defaultVariationValue: options.defaultVariationValue,
207
+ defaultVariableValue: options.defaultVariableValue,
208
+ };
209
+ }
347
210
 
348
- bucketKeySeparator: this.bucketKeySeparator,
349
- configureBucketKey: this.configureBucketKey,
350
- configureBucketValue: this.configureBucketValue,
211
+ evaluateFlag(
212
+ featureKey: FeatureKey,
213
+ context: Context = {},
214
+ options: OverrideOptions = {},
215
+ ): Evaluation {
216
+ return evaluateWithHooks({
217
+ ...this.getEvaluationDependencies(context, options),
218
+ type: "flag",
219
+ featureKey,
351
220
  });
352
221
  }
353
222
 
354
- isEnabled(featureKey: FeatureKey | Feature, context: Context = {}): boolean {
223
+ isEnabled(featureKey: FeatureKey, context: Context = {}, options: OverrideOptions = {}): boolean {
355
224
  try {
356
- const evaluation = this.evaluateFlag(featureKey, context);
225
+ const evaluation = this.evaluateFlag(featureKey, context, options);
357
226
 
358
227
  return evaluation.enabled === true;
359
228
  } catch (e) {
@@ -366,33 +235,25 @@ export class FeaturevisorInstance {
366
235
  /**
367
236
  * Variation
368
237
  */
369
- evaluateVariation(featureKey: FeatureKey | Feature, context: Context = {}): Evaluation {
370
- return evaluate({
238
+ evaluateVariation(
239
+ featureKey: FeatureKey,
240
+ context: Context = {},
241
+ options: OverrideOptions = {},
242
+ ): Evaluation {
243
+ return evaluateWithHooks({
244
+ ...this.getEvaluationDependencies(context, options),
371
245
  type: "variation",
372
-
373
246
  featureKey,
374
- context,
375
-
376
- logger: this.logger,
377
- datafileReader: this.datafileReader,
378
- statuses: this.statuses,
379
- interceptContext: this.interceptContext,
380
-
381
- stickyFeatures: this.stickyFeatures,
382
- initialFeatures: this.initialFeatures,
383
-
384
- bucketKeySeparator: this.bucketKeySeparator,
385
- configureBucketKey: this.configureBucketKey,
386
- configureBucketValue: this.configureBucketValue,
387
247
  });
388
248
  }
389
249
 
390
250
  getVariation(
391
- featureKey: FeatureKey | Feature,
251
+ featureKey: FeatureKey,
392
252
  context: Context = {},
393
- ): VariationValue | undefined {
253
+ options: OverrideOptions = {},
254
+ ): VariationValue | null {
394
255
  try {
395
- const evaluation = this.evaluateVariation(featureKey, context);
256
+ const evaluation = this.evaluateVariation(featureKey, context, options);
396
257
 
397
258
  if (typeof evaluation.variationValue !== "undefined") {
398
259
  return evaluation.variationValue;
@@ -402,56 +263,11 @@ export class FeaturevisorInstance {
402
263
  return evaluation.variation.value;
403
264
  }
404
265
 
405
- return undefined;
266
+ return null;
406
267
  } catch (e) {
407
268
  this.logger.error("getVariation", { featureKey, error: e });
408
269
 
409
- return undefined;
410
- }
411
- }
412
-
413
- /**
414
- * Activate
415
- */
416
- activate(featureKey: FeatureKey, context: Context = {}): VariationValue | undefined {
417
- try {
418
- const evaluation = this.evaluateVariation(featureKey, context);
419
- const variationValue = evaluation.variation
420
- ? evaluation.variation.value
421
- : evaluation.variationValue;
422
-
423
- if (typeof variationValue === "undefined") {
424
- return undefined;
425
- }
426
-
427
- const finalContext = this.interceptContext ? this.interceptContext(context) : context;
428
-
429
- const captureContext: Context = {};
430
-
431
- const attributesForCapturing = this.datafileReader
432
- .getAllAttributes()
433
- .filter((a) => a.capture === true);
434
-
435
- attributesForCapturing.forEach((a) => {
436
- if (typeof finalContext[a.key] !== "undefined") {
437
- captureContext[a.key] = context[a.key];
438
- }
439
- });
440
-
441
- this.emitter.emit(
442
- "activation",
443
- featureKey,
444
- variationValue,
445
- finalContext,
446
- captureContext,
447
- evaluation,
448
- );
449
-
450
- return variationValue;
451
- } catch (e) {
452
- this.logger.error("activate", { featureKey, error: e });
453
-
454
- return undefined;
270
+ return null;
455
271
  }
456
272
  }
457
273
 
@@ -459,38 +275,27 @@ export class FeaturevisorInstance {
459
275
  * Variable
460
276
  */
461
277
  evaluateVariable(
462
- featureKey: FeatureKey | Feature,
278
+ featureKey: FeatureKey,
463
279
  variableKey: VariableKey,
464
280
  context: Context = {},
281
+ options: OverrideOptions = {},
465
282
  ): Evaluation {
466
- return evaluate({
283
+ return evaluateWithHooks({
284
+ ...this.getEvaluationDependencies(context, options),
467
285
  type: "variable",
468
-
469
286
  featureKey,
470
287
  variableKey,
471
- context,
472
-
473
- logger: this.logger,
474
- datafileReader: this.datafileReader,
475
- statuses: this.statuses,
476
- interceptContext: this.interceptContext,
477
-
478
- stickyFeatures: this.stickyFeatures,
479
- initialFeatures: this.initialFeatures,
480
-
481
- bucketKeySeparator: this.bucketKeySeparator,
482
- configureBucketKey: this.configureBucketKey,
483
- configureBucketValue: this.configureBucketValue,
484
288
  });
485
289
  }
486
290
 
487
291
  getVariable(
488
- featureKey: FeatureKey | Feature,
292
+ featureKey: FeatureKey,
489
293
  variableKey: string,
490
294
  context: Context = {},
491
- ): VariableValue | undefined {
295
+ options: OverrideOptions = {},
296
+ ): VariableValue | null {
492
297
  try {
493
- const evaluation = this.evaluateVariable(featureKey, variableKey, context);
298
+ const evaluation = this.evaluateVariable(featureKey, variableKey, context, options);
494
299
 
495
300
  if (typeof evaluation.variableValue !== "undefined") {
496
301
  if (
@@ -504,82 +309,133 @@ export class FeaturevisorInstance {
504
309
  return evaluation.variableValue;
505
310
  }
506
311
 
507
- return undefined;
312
+ return null;
508
313
  } catch (e) {
509
314
  this.logger.error("getVariable", { featureKey, variableKey, error: e });
510
315
 
511
- return undefined;
316
+ return null;
512
317
  }
513
318
  }
514
319
 
515
320
  getVariableBoolean(
516
- featureKey: FeatureKey | Feature,
321
+ featureKey: FeatureKey,
517
322
  variableKey: string,
518
323
  context: Context = {},
519
- ): boolean | undefined {
520
- const variableValue = this.getVariable(featureKey, variableKey, context);
324
+ options: OverrideOptions = {},
325
+ ): boolean | null {
326
+ const variableValue = this.getVariable(featureKey, variableKey, context, options);
521
327
 
522
- return getValueByType(variableValue, "boolean") as boolean | undefined;
328
+ return getValueByType(variableValue, "boolean") as boolean | null;
523
329
  }
524
330
 
525
331
  getVariableString(
526
- featureKey: FeatureKey | Feature,
332
+ featureKey: FeatureKey,
527
333
  variableKey: string,
528
334
  context: Context = {},
529
- ): string | undefined {
530
- const variableValue = this.getVariable(featureKey, variableKey, context);
335
+ options: OverrideOptions = {},
336
+ ): string | null {
337
+ const variableValue = this.getVariable(featureKey, variableKey, context, options);
531
338
 
532
- return getValueByType(variableValue, "string") as string | undefined;
339
+ return getValueByType(variableValue, "string") as string | null;
533
340
  }
534
341
 
535
342
  getVariableInteger(
536
- featureKey: FeatureKey | Feature,
343
+ featureKey: FeatureKey,
537
344
  variableKey: string,
538
345
  context: Context = {},
539
- ): number | undefined {
540
- const variableValue = this.getVariable(featureKey, variableKey, context);
346
+ options: OverrideOptions = {},
347
+ ): number | null {
348
+ const variableValue = this.getVariable(featureKey, variableKey, context, options);
541
349
 
542
- return getValueByType(variableValue, "integer") as number | undefined;
350
+ return getValueByType(variableValue, "integer") as number | null;
543
351
  }
544
352
 
545
353
  getVariableDouble(
546
- featureKey: FeatureKey | Feature,
354
+ featureKey: FeatureKey,
547
355
  variableKey: string,
548
356
  context: Context = {},
549
- ): number | undefined {
550
- const variableValue = this.getVariable(featureKey, variableKey, context);
357
+ options: OverrideOptions = {},
358
+ ): number | null {
359
+ const variableValue = this.getVariable(featureKey, variableKey, context, options);
551
360
 
552
- return getValueByType(variableValue, "double") as number | undefined;
361
+ return getValueByType(variableValue, "double") as number | null;
553
362
  }
554
363
 
555
364
  getVariableArray(
556
- featureKey: FeatureKey | Feature,
365
+ featureKey: FeatureKey,
557
366
  variableKey: string,
558
367
  context: Context = {},
559
- ): string[] | undefined {
560
- const variableValue = this.getVariable(featureKey, variableKey, context);
368
+ options: OverrideOptions = {},
369
+ ): string[] | null {
370
+ const variableValue = this.getVariable(featureKey, variableKey, context, options);
561
371
 
562
- return getValueByType(variableValue, "array") as string[] | undefined;
372
+ return getValueByType(variableValue, "array") as string[] | null;
563
373
  }
564
374
 
565
375
  getVariableObject<T>(
566
- featureKey: FeatureKey | Feature,
376
+ featureKey: FeatureKey,
567
377
  variableKey: string,
568
378
  context: Context = {},
569
- ): T | undefined {
570
- const variableValue = this.getVariable(featureKey, variableKey, context);
379
+ options: OverrideOptions = {},
380
+ ): T | null {
381
+ const variableValue = this.getVariable(featureKey, variableKey, context, options);
571
382
 
572
- return getValueByType(variableValue, "object") as T | undefined;
383
+ return getValueByType(variableValue, "object") as T | null;
573
384
  }
574
385
 
575
386
  getVariableJSON<T>(
576
- featureKey: FeatureKey | Feature,
387
+ featureKey: FeatureKey,
577
388
  variableKey: string,
578
389
  context: Context = {},
579
- ): T | undefined {
580
- const variableValue = this.getVariable(featureKey, variableKey, context);
390
+ options: OverrideOptions = {},
391
+ ): T | null {
392
+ const variableValue = this.getVariable(featureKey, variableKey, context, options);
393
+
394
+ return getValueByType(variableValue, "json") as T | null;
395
+ }
396
+
397
+ getAllEvaluations(
398
+ context: Context = {},
399
+ featureKeys: string[] = [],
400
+ options: OverrideOptions = {},
401
+ ): EvaluatedFeatures {
402
+ const result: EvaluatedFeatures = {};
403
+
404
+ const keys = featureKeys.length > 0 ? featureKeys : this.datafileReader.getFeatureKeys();
405
+ for (const featureKey of keys) {
406
+ // isEnabled
407
+ const evaluatedFeature: EvaluatedFeature = {
408
+ enabled: this.isEnabled(featureKey, context, options),
409
+ };
410
+
411
+ // variation
412
+ if (this.datafileReader.hasVariations(featureKey)) {
413
+ const variation = this.getVariation(featureKey, context, options);
414
+
415
+ if (variation) {
416
+ evaluatedFeature.variation = variation;
417
+ }
418
+ }
419
+
420
+ // variables
421
+ const variableKeys = this.datafileReader.getVariableKeys(featureKey);
422
+ if (variableKeys.length > 0) {
423
+ evaluatedFeature.variables = {};
424
+
425
+ for (const variableKey of variableKeys) {
426
+ evaluatedFeature.variables[variableKey] = this.getVariable(
427
+ featureKey,
428
+ variableKey,
429
+ context,
430
+ options,
431
+ );
432
+ }
433
+ }
434
+
435
+ result[featureKey] = evaluatedFeature;
436
+ }
581
437
 
582
- return getValueByType(variableValue, "json") as T | undefined;
438
+ return result;
583
439
  }
584
440
  }
585
441