@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.
- package/CHANGELOG.md +8 -0
- package/README.md +2 -381
- package/coverage/clover.xml +707 -645
- package/coverage/coverage-final.json +11 -9
- package/coverage/lcov-report/{segments.ts.html → bucketer.ts.html} +155 -77
- package/coverage/lcov-report/child.ts.html +940 -0
- package/coverage/lcov-report/conditions.ts.html +107 -158
- package/coverage/lcov-report/datafileReader.ts.html +763 -103
- package/coverage/lcov-report/emitter.ts.html +77 -59
- package/coverage/lcov-report/evaluate.ts.html +689 -416
- package/coverage/lcov-report/events.ts.html +334 -0
- package/coverage/lcov-report/helpers.ts.html +184 -0
- package/coverage/lcov-report/{bucket.ts.html → hooks.ts.html} +86 -239
- package/coverage/lcov-report/index.html +119 -89
- package/coverage/lcov-report/instance.ts.html +341 -773
- package/coverage/lcov-report/logger.ts.html +64 -64
- package/coverage/lcov.info +1433 -1226
- package/dist/bucketer.d.ts +11 -0
- package/dist/child.d.ts +26 -0
- package/dist/compareVersions.d.ts +4 -0
- package/dist/conditions.d.ts +4 -4
- package/dist/datafileReader.d.ts +26 -6
- package/dist/emitter.d.ts +8 -9
- package/dist/evaluate.d.ts +31 -29
- package/dist/events.d.ts +5 -0
- package/dist/helpers.d.ts +5 -0
- package/dist/hooks.d.ts +45 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.gz +0 -0
- package/dist/index.mjs.map +1 -1
- package/dist/instance.d.ts +40 -72
- package/dist/logger.d.ts +6 -5
- package/dist/murmurhash.d.ts +1 -0
- package/jest.config.js +2 -0
- package/lib/bucketer.d.ts +11 -0
- package/lib/child.d.ts +26 -0
- package/lib/compareVersions.d.ts +4 -0
- package/lib/conditions.d.ts +4 -4
- package/lib/datafileReader.d.ts +26 -6
- package/lib/emitter.d.ts +8 -9
- package/lib/evaluate.d.ts +31 -29
- package/lib/events.d.ts +5 -0
- package/lib/helpers.d.ts +5 -0
- package/lib/hooks.d.ts +45 -0
- package/lib/index.d.ts +3 -2
- package/lib/instance.d.ts +40 -72
- package/lib/logger.d.ts +6 -5
- package/lib/murmurhash.d.ts +1 -0
- package/package.json +3 -5
- package/src/bucketer.spec.ts +165 -0
- package/src/bucketer.ts +84 -0
- package/src/child.spec.ts +267 -0
- package/src/child.ts +285 -0
- package/src/compareVersions.ts +93 -0
- package/src/conditions.spec.ts +563 -353
- package/src/conditions.ts +46 -63
- package/src/datafileReader.spec.ts +396 -84
- package/src/datafileReader.ts +280 -60
- package/src/emitter.spec.ts +27 -86
- package/src/emitter.ts +38 -32
- package/src/evaluate.ts +349 -258
- package/src/events.spec.ts +154 -0
- package/src/events.ts +83 -0
- package/src/helpers.ts +33 -0
- package/src/hooks.ts +88 -0
- package/src/index.ts +3 -2
- package/src/instance.spec.ts +305 -489
- package/src/instance.ts +247 -391
- package/src/logger.spec.ts +212 -134
- package/src/logger.ts +36 -36
- package/src/murmurhash.ts +71 -0
- package/coverage/lcov-report/feature.ts.html +0 -508
- package/dist/bucket.d.ts +0 -30
- package/dist/feature.d.ts +0 -16
- package/dist/segments.d.ts +0 -5
- package/lib/bucket.d.ts +0 -30
- package/lib/feature.d.ts +0 -16
- package/lib/segments.d.ts +0 -5
- package/src/bucket.spec.ts +0 -37
- package/src/bucket.ts +0 -139
- package/src/feature.ts +0 -141
- package/src/segments.spec.ts +0 -468
- 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
|
-
|
|
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 {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
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: "
|
|
24
|
+
schemaVersion: "2",
|
|
58
25
|
revision: "unknown",
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
features: [],
|
|
26
|
+
segments: {},
|
|
27
|
+
features: {},
|
|
62
28
|
};
|
|
63
29
|
|
|
64
|
-
export
|
|
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
|
-
|
|
33
|
+
defaultVariationValue?: VariationValue;
|
|
34
|
+
defaultVariableValue?: VariableValue;
|
|
75
35
|
}
|
|
76
36
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
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
|
|
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.
|
|
137
|
-
this.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
this.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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.
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
88
|
+
setLogLevel(level: LogLevel) {
|
|
89
|
+
this.logger.setLevel(level);
|
|
90
|
+
}
|
|
181
91
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
187
|
-
.then((datafile) => {
|
|
188
|
-
this.setDatafile(datafile);
|
|
99
|
+
const details = getParamsForDatafileSetEvent(this.datafileReader, newDatafileReader);
|
|
189
100
|
|
|
190
|
-
|
|
191
|
-
this.emitter.emit("ready");
|
|
101
|
+
this.datafileReader = newDatafileReader;
|
|
192
102
|
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
215
|
-
this.
|
|
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
|
-
|
|
113
|
+
if (replace) {
|
|
114
|
+
this.sticky = { ...sticky };
|
|
115
|
+
} else {
|
|
116
|
+
this.sticky = {
|
|
117
|
+
...this.sticky,
|
|
118
|
+
...sticky,
|
|
228
119
|
};
|
|
120
|
+
}
|
|
229
121
|
|
|
230
|
-
|
|
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
|
-
|
|
235
|
-
|
|
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
|
-
|
|
245
|
-
this.
|
|
132
|
+
getFeature(featureKey: string): Feature | undefined {
|
|
133
|
+
return this.datafileReader.getFeature(featureKey);
|
|
246
134
|
}
|
|
247
135
|
|
|
248
|
-
|
|
249
|
-
return this.
|
|
136
|
+
addHook(hook: Hook) {
|
|
137
|
+
return this.hooksManager.add(hook);
|
|
250
138
|
}
|
|
251
139
|
|
|
252
|
-
|
|
253
|
-
return
|
|
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
|
-
|
|
260
|
-
*/
|
|
261
|
-
isReady(): boolean {
|
|
262
|
-
return this.statuses.ready;
|
|
144
|
+
close() {
|
|
145
|
+
this.emitter.clearAll();
|
|
263
146
|
}
|
|
264
147
|
|
|
265
148
|
/**
|
|
266
|
-
*
|
|
149
|
+
* Context
|
|
267
150
|
*/
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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.
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
346
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
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(
|
|
370
|
-
|
|
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
|
|
251
|
+
featureKey: FeatureKey,
|
|
392
252
|
context: Context = {},
|
|
393
|
-
|
|
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
|
|
266
|
+
return null;
|
|
406
267
|
} catch (e) {
|
|
407
268
|
this.logger.error("getVariation", { featureKey, error: e });
|
|
408
269
|
|
|
409
|
-
return
|
|
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
|
|
278
|
+
featureKey: FeatureKey,
|
|
463
279
|
variableKey: VariableKey,
|
|
464
280
|
context: Context = {},
|
|
281
|
+
options: OverrideOptions = {},
|
|
465
282
|
): Evaluation {
|
|
466
|
-
return
|
|
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
|
|
292
|
+
featureKey: FeatureKey,
|
|
489
293
|
variableKey: string,
|
|
490
294
|
context: Context = {},
|
|
491
|
-
|
|
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
|
|
312
|
+
return null;
|
|
508
313
|
} catch (e) {
|
|
509
314
|
this.logger.error("getVariable", { featureKey, variableKey, error: e });
|
|
510
315
|
|
|
511
|
-
return
|
|
316
|
+
return null;
|
|
512
317
|
}
|
|
513
318
|
}
|
|
514
319
|
|
|
515
320
|
getVariableBoolean(
|
|
516
|
-
featureKey: FeatureKey
|
|
321
|
+
featureKey: FeatureKey,
|
|
517
322
|
variableKey: string,
|
|
518
323
|
context: Context = {},
|
|
519
|
-
|
|
520
|
-
|
|
324
|
+
options: OverrideOptions = {},
|
|
325
|
+
): boolean | null {
|
|
326
|
+
const variableValue = this.getVariable(featureKey, variableKey, context, options);
|
|
521
327
|
|
|
522
|
-
return getValueByType(variableValue, "boolean") as boolean |
|
|
328
|
+
return getValueByType(variableValue, "boolean") as boolean | null;
|
|
523
329
|
}
|
|
524
330
|
|
|
525
331
|
getVariableString(
|
|
526
|
-
featureKey: FeatureKey
|
|
332
|
+
featureKey: FeatureKey,
|
|
527
333
|
variableKey: string,
|
|
528
334
|
context: Context = {},
|
|
529
|
-
|
|
530
|
-
|
|
335
|
+
options: OverrideOptions = {},
|
|
336
|
+
): string | null {
|
|
337
|
+
const variableValue = this.getVariable(featureKey, variableKey, context, options);
|
|
531
338
|
|
|
532
|
-
return getValueByType(variableValue, "string") as string |
|
|
339
|
+
return getValueByType(variableValue, "string") as string | null;
|
|
533
340
|
}
|
|
534
341
|
|
|
535
342
|
getVariableInteger(
|
|
536
|
-
featureKey: FeatureKey
|
|
343
|
+
featureKey: FeatureKey,
|
|
537
344
|
variableKey: string,
|
|
538
345
|
context: Context = {},
|
|
539
|
-
|
|
540
|
-
|
|
346
|
+
options: OverrideOptions = {},
|
|
347
|
+
): number | null {
|
|
348
|
+
const variableValue = this.getVariable(featureKey, variableKey, context, options);
|
|
541
349
|
|
|
542
|
-
return getValueByType(variableValue, "integer") as number |
|
|
350
|
+
return getValueByType(variableValue, "integer") as number | null;
|
|
543
351
|
}
|
|
544
352
|
|
|
545
353
|
getVariableDouble(
|
|
546
|
-
featureKey: FeatureKey
|
|
354
|
+
featureKey: FeatureKey,
|
|
547
355
|
variableKey: string,
|
|
548
356
|
context: Context = {},
|
|
549
|
-
|
|
550
|
-
|
|
357
|
+
options: OverrideOptions = {},
|
|
358
|
+
): number | null {
|
|
359
|
+
const variableValue = this.getVariable(featureKey, variableKey, context, options);
|
|
551
360
|
|
|
552
|
-
return getValueByType(variableValue, "double") as number |
|
|
361
|
+
return getValueByType(variableValue, "double") as number | null;
|
|
553
362
|
}
|
|
554
363
|
|
|
555
364
|
getVariableArray(
|
|
556
|
-
featureKey: FeatureKey
|
|
365
|
+
featureKey: FeatureKey,
|
|
557
366
|
variableKey: string,
|
|
558
367
|
context: Context = {},
|
|
559
|
-
|
|
560
|
-
|
|
368
|
+
options: OverrideOptions = {},
|
|
369
|
+
): string[] | null {
|
|
370
|
+
const variableValue = this.getVariable(featureKey, variableKey, context, options);
|
|
561
371
|
|
|
562
|
-
return getValueByType(variableValue, "array") as string[] |
|
|
372
|
+
return getValueByType(variableValue, "array") as string[] | null;
|
|
563
373
|
}
|
|
564
374
|
|
|
565
375
|
getVariableObject<T>(
|
|
566
|
-
featureKey: FeatureKey
|
|
376
|
+
featureKey: FeatureKey,
|
|
567
377
|
variableKey: string,
|
|
568
378
|
context: Context = {},
|
|
569
|
-
|
|
570
|
-
|
|
379
|
+
options: OverrideOptions = {},
|
|
380
|
+
): T | null {
|
|
381
|
+
const variableValue = this.getVariable(featureKey, variableKey, context, options);
|
|
571
382
|
|
|
572
|
-
return getValueByType(variableValue, "object") as T |
|
|
383
|
+
return getValueByType(variableValue, "object") as T | null;
|
|
573
384
|
}
|
|
574
385
|
|
|
575
386
|
getVariableJSON<T>(
|
|
576
|
-
featureKey: FeatureKey
|
|
387
|
+
featureKey: FeatureKey,
|
|
577
388
|
variableKey: string,
|
|
578
389
|
context: Context = {},
|
|
579
|
-
|
|
580
|
-
|
|
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
|
|
438
|
+
return result;
|
|
583
439
|
}
|
|
584
440
|
}
|
|
585
441
|
|