@featurevisor/sdk 0.8.0 → 0.9.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 +11 -0
- package/dist/index.js +1 -1
- package/dist/index.js.gz +0 -0
- package/dist/index.js.map +1 -1
- package/lib/client.d.ts +3 -0
- package/lib/client.js +22 -8
- package/lib/client.js.map +1 -1
- package/lib/createInstance.d.ts +2 -0
- package/lib/createInstance.js +5 -1
- package/lib/createInstance.js.map +1 -1
- package/lib/feature.d.ts +4 -3
- package/lib/feature.js +67 -5
- package/lib/feature.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/logger.d.ts +25 -0
- package/lib/logger.js +57 -0
- package/lib/logger.js.map +1 -0
- package/package.json +3 -3
- package/src/client.ts +37 -8
- package/src/createInstance.ts +7 -1
- package/src/feature.ts +80 -0
- package/src/index.ts +1 -0
- package/src/logger.ts +86 -0
package/src/client.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
getForcedVariableValue,
|
|
19
19
|
} from "./feature";
|
|
20
20
|
import { getBucketedNumber } from "./bucket";
|
|
21
|
-
import {
|
|
21
|
+
import { createLogger, Logger } from "./logger";
|
|
22
22
|
|
|
23
23
|
export type ActivationCallback = (
|
|
24
24
|
featureName: string,
|
|
@@ -33,6 +33,7 @@ export interface SdkOptions {
|
|
|
33
33
|
datafile: DatafileContent | string;
|
|
34
34
|
onActivation?: ActivationCallback; // @TODO: move it to FeaturevisorInstance in next breaking semver
|
|
35
35
|
configureBucketValue?: ConfigureBucketValue;
|
|
36
|
+
logger?: Logger; // TODO: keep it in FeaturevisorInstance only in next breaking semver
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
type FieldType = VariationType | VariableType;
|
|
@@ -68,6 +69,7 @@ export class FeaturevisorSDK {
|
|
|
68
69
|
private onActivation?: ActivationCallback;
|
|
69
70
|
private datafileReader: DatafileReader;
|
|
70
71
|
private configureBucketValue?: ConfigureBucketValue;
|
|
72
|
+
private logger: Logger;
|
|
71
73
|
|
|
72
74
|
constructor(options: SdkOptions) {
|
|
73
75
|
if (options.onActivation) {
|
|
@@ -78,6 +80,8 @@ export class FeaturevisorSDK {
|
|
|
78
80
|
this.configureBucketValue = options.configureBucketValue;
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
this.logger = options.logger || createLogger();
|
|
84
|
+
|
|
81
85
|
this.setDatafile(options.datafile);
|
|
82
86
|
}
|
|
83
87
|
|
|
@@ -87,8 +91,7 @@ export class FeaturevisorSDK {
|
|
|
87
91
|
typeof datafile === "string" ? JSON.parse(datafile) : datafile,
|
|
88
92
|
);
|
|
89
93
|
} catch (e) {
|
|
90
|
-
|
|
91
|
-
console.error(e);
|
|
94
|
+
this.logger.error("could not parse datafile", { error: e });
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
|
|
@@ -135,26 +138,45 @@ export class FeaturevisorSDK {
|
|
|
135
138
|
const feature = this.getFeature(featureKey);
|
|
136
139
|
|
|
137
140
|
if (!feature) {
|
|
141
|
+
this.logger.warn("feature not found in datafile", { featureKey });
|
|
142
|
+
|
|
138
143
|
return undefined;
|
|
139
144
|
}
|
|
140
145
|
|
|
141
146
|
const forcedVariation = getForcedVariation(feature, attributes, this.datafileReader);
|
|
142
147
|
|
|
143
148
|
if (forcedVariation) {
|
|
149
|
+
this.logger.debug("forced variation found", {
|
|
150
|
+
featureKey,
|
|
151
|
+
variation: forcedVariation.value,
|
|
152
|
+
});
|
|
153
|
+
|
|
144
154
|
return forcedVariation.value;
|
|
145
155
|
}
|
|
146
156
|
|
|
147
157
|
const bucketValue = this.getBucketValue(feature, attributes);
|
|
148
158
|
|
|
149
|
-
const variation = getBucketedVariation(
|
|
159
|
+
const variation = getBucketedVariation(
|
|
160
|
+
feature,
|
|
161
|
+
attributes,
|
|
162
|
+
bucketValue,
|
|
163
|
+
this.datafileReader,
|
|
164
|
+
this.logger,
|
|
165
|
+
);
|
|
150
166
|
|
|
151
167
|
if (!variation) {
|
|
168
|
+
this.logger.debug("using default variation", {
|
|
169
|
+
featureKey,
|
|
170
|
+
bucketValue,
|
|
171
|
+
variation: feature.defaultVariation,
|
|
172
|
+
});
|
|
173
|
+
|
|
152
174
|
return feature.defaultVariation;
|
|
153
175
|
}
|
|
154
176
|
|
|
155
177
|
return variation.value;
|
|
156
178
|
} catch (e) {
|
|
157
|
-
|
|
179
|
+
this.logger.error("getVariation", { featureKey, error: e });
|
|
158
180
|
|
|
159
181
|
return undefined;
|
|
160
182
|
}
|
|
@@ -203,7 +225,7 @@ export class FeaturevisorSDK {
|
|
|
203
225
|
try {
|
|
204
226
|
const variationValue = this.getVariation(featureKey, attributes);
|
|
205
227
|
|
|
206
|
-
if (
|
|
228
|
+
if (typeof variationValue === "undefined") {
|
|
207
229
|
return undefined;
|
|
208
230
|
}
|
|
209
231
|
|
|
@@ -225,7 +247,7 @@ export class FeaturevisorSDK {
|
|
|
225
247
|
|
|
226
248
|
return variationValue;
|
|
227
249
|
} catch (e) {
|
|
228
|
-
|
|
250
|
+
this.logger.error("activate", { featureKey, error: e });
|
|
229
251
|
|
|
230
252
|
return undefined;
|
|
231
253
|
}
|
|
@@ -268,6 +290,8 @@ export class FeaturevisorSDK {
|
|
|
268
290
|
const feature = this.getFeature(featureKey);
|
|
269
291
|
|
|
270
292
|
if (!feature) {
|
|
293
|
+
this.logger.warn("feature not found in datafile", { featureKey, variableKey });
|
|
294
|
+
|
|
271
295
|
return undefined;
|
|
272
296
|
}
|
|
273
297
|
|
|
@@ -276,6 +300,8 @@ export class FeaturevisorSDK {
|
|
|
276
300
|
: undefined;
|
|
277
301
|
|
|
278
302
|
if (!variableSchema) {
|
|
303
|
+
this.logger.warn("variable schema not found", { featureKey, variableKey });
|
|
304
|
+
|
|
279
305
|
return undefined;
|
|
280
306
|
}
|
|
281
307
|
|
|
@@ -287,6 +313,8 @@ export class FeaturevisorSDK {
|
|
|
287
313
|
);
|
|
288
314
|
|
|
289
315
|
if (typeof forcedVariableValue !== "undefined") {
|
|
316
|
+
this.logger.debug("forced variable value found", { featureKey, variableKey });
|
|
317
|
+
|
|
290
318
|
return forcedVariableValue;
|
|
291
319
|
}
|
|
292
320
|
|
|
@@ -298,9 +326,10 @@ export class FeaturevisorSDK {
|
|
|
298
326
|
attributes,
|
|
299
327
|
bucketValue,
|
|
300
328
|
this.datafileReader,
|
|
329
|
+
this.logger,
|
|
301
330
|
);
|
|
302
331
|
} catch (e) {
|
|
303
|
-
|
|
332
|
+
this.logger.error("getVariable", { featureKey, variableKey, error: e });
|
|
304
333
|
|
|
305
334
|
return undefined;
|
|
306
335
|
}
|
package/src/createInstance.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DatafileContent } from "@featurevisor/types";
|
|
2
2
|
import { FeaturevisorSDK, ConfigureBucketValue, ActivationCallback } from "./client";
|
|
3
|
+
import { createLogger, Logger } from "./logger";
|
|
3
4
|
|
|
4
5
|
export type ReadyCallback = () => void;
|
|
5
6
|
|
|
@@ -13,6 +14,7 @@ export interface InstanceOptions {
|
|
|
13
14
|
datafileUrl?: string;
|
|
14
15
|
onReady?: ReadyCallback;
|
|
15
16
|
handleDatafileFetch?: (datafileUrl: string) => Promise<DatafileContent>;
|
|
17
|
+
logger?: Logger;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
// @TODO: consider renaming it to FeaturevisorSDK in next breaking semver
|
|
@@ -106,16 +108,19 @@ function fetchDatafileContent(datafileUrl, options: InstanceOptions): Promise<Da
|
|
|
106
108
|
export function createInstance(options: InstanceOptions) {
|
|
107
109
|
if (!options.datafile && !options.datafileUrl) {
|
|
108
110
|
throw new Error(
|
|
109
|
-
"Featurevisor SDK instance cannot be created without `datafile`
|
|
111
|
+
"Featurevisor SDK instance cannot be created without both `datafile` and `datafileUrl` options",
|
|
110
112
|
);
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
const logger = options.logger || createLogger();
|
|
116
|
+
|
|
113
117
|
// datafile content is already provided
|
|
114
118
|
if (options.datafile) {
|
|
115
119
|
const sdk = new FeaturevisorSDK({
|
|
116
120
|
datafile: options.datafile,
|
|
117
121
|
onActivation: options.onActivation,
|
|
118
122
|
configureBucketValue: options.configureBucketValue,
|
|
123
|
+
logger,
|
|
119
124
|
});
|
|
120
125
|
|
|
121
126
|
if (typeof options.onReady === "function") {
|
|
@@ -134,6 +139,7 @@ export function createInstance(options: InstanceOptions) {
|
|
|
134
139
|
datafile: emptyDatafile,
|
|
135
140
|
onActivation: options.onActivation,
|
|
136
141
|
configureBucketValue: options.configureBucketValue,
|
|
142
|
+
logger,
|
|
137
143
|
});
|
|
138
144
|
|
|
139
145
|
if (options.datafileUrl) {
|
package/src/feature.ts
CHANGED
|
@@ -11,12 +11,14 @@ import { DatafileReader } from "./datafileReader";
|
|
|
11
11
|
import { allGroupSegmentsAreMatched } from "./segments";
|
|
12
12
|
import { allConditionsAreMatched } from "./conditions";
|
|
13
13
|
import { VariableSchema } from "@featurevisor/types/src";
|
|
14
|
+
import { Logger } from "./logger";
|
|
14
15
|
|
|
15
16
|
export function getMatchedTraffic(
|
|
16
17
|
traffic: Traffic[],
|
|
17
18
|
attributes: Attributes,
|
|
18
19
|
bucketValue: number,
|
|
19
20
|
datafileReader: DatafileReader,
|
|
21
|
+
logger: Logger,
|
|
20
22
|
): Traffic | undefined {
|
|
21
23
|
return traffic.find((traffic) => {
|
|
22
24
|
if (bucketValue > traffic.percentage) {
|
|
@@ -36,6 +38,10 @@ export function getMatchedTraffic(
|
|
|
36
38
|
return false;
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
logger.debug("matched rule", {
|
|
42
|
+
ruleKey: traffic.key,
|
|
43
|
+
});
|
|
44
|
+
|
|
39
45
|
return true;
|
|
40
46
|
});
|
|
41
47
|
}
|
|
@@ -99,21 +105,33 @@ export function getBucketedVariation(
|
|
|
99
105
|
attributes: Attributes,
|
|
100
106
|
bucketValue: number,
|
|
101
107
|
datafileReader: DatafileReader,
|
|
108
|
+
logger: Logger,
|
|
102
109
|
): Variation | undefined {
|
|
103
110
|
const matchedTraffic = getMatchedTraffic(
|
|
104
111
|
feature.traffic,
|
|
105
112
|
attributes,
|
|
106
113
|
bucketValue,
|
|
107
114
|
datafileReader,
|
|
115
|
+
logger,
|
|
108
116
|
);
|
|
109
117
|
|
|
110
118
|
if (!matchedTraffic) {
|
|
119
|
+
logger.debug("no matched rule found", {
|
|
120
|
+
featureKey: feature.key,
|
|
121
|
+
bucketValue,
|
|
122
|
+
});
|
|
123
|
+
|
|
111
124
|
return undefined;
|
|
112
125
|
}
|
|
113
126
|
|
|
114
127
|
const allocation = getMatchedAllocation(matchedTraffic, bucketValue);
|
|
115
128
|
|
|
116
129
|
if (!allocation) {
|
|
130
|
+
logger.debug("no matched allocation found", {
|
|
131
|
+
featureKey: feature.key,
|
|
132
|
+
bucketValue,
|
|
133
|
+
});
|
|
134
|
+
|
|
117
135
|
return undefined;
|
|
118
136
|
}
|
|
119
137
|
|
|
@@ -124,9 +142,22 @@ export function getBucketedVariation(
|
|
|
124
142
|
});
|
|
125
143
|
|
|
126
144
|
if (!variation) {
|
|
145
|
+
// this should never happen
|
|
146
|
+
logger.debug("no matched variation found", {
|
|
147
|
+
featureKey: feature.key,
|
|
148
|
+
variation: variationValue,
|
|
149
|
+
bucketValue,
|
|
150
|
+
});
|
|
151
|
+
|
|
127
152
|
return undefined;
|
|
128
153
|
}
|
|
129
154
|
|
|
155
|
+
logger.debug("matched variation", {
|
|
156
|
+
featureKey: feature.key,
|
|
157
|
+
variation: variation.value,
|
|
158
|
+
bucketValue,
|
|
159
|
+
});
|
|
160
|
+
|
|
130
161
|
return variation;
|
|
131
162
|
}
|
|
132
163
|
|
|
@@ -157,6 +188,7 @@ export function getBucketedVariableValue(
|
|
|
157
188
|
attributes: Attributes,
|
|
158
189
|
bucketValue: number,
|
|
159
190
|
datafileReader: DatafileReader,
|
|
191
|
+
logger: Logger,
|
|
160
192
|
): VariableValue | undefined {
|
|
161
193
|
// get traffic
|
|
162
194
|
const matchedTraffic = getMatchedTraffic(
|
|
@@ -164,9 +196,16 @@ export function getBucketedVariableValue(
|
|
|
164
196
|
attributes,
|
|
165
197
|
bucketValue,
|
|
166
198
|
datafileReader,
|
|
199
|
+
logger,
|
|
167
200
|
);
|
|
168
201
|
|
|
169
202
|
if (!matchedTraffic) {
|
|
203
|
+
logger.debug("no matched rule found", {
|
|
204
|
+
featureKey: feature.key,
|
|
205
|
+
variableKey: variableSchema.key,
|
|
206
|
+
bucketValue,
|
|
207
|
+
});
|
|
208
|
+
|
|
170
209
|
return undefined;
|
|
171
210
|
}
|
|
172
211
|
|
|
@@ -174,12 +213,24 @@ export function getBucketedVariableValue(
|
|
|
174
213
|
|
|
175
214
|
// see if variable is set at traffic/rule level
|
|
176
215
|
if (matchedTraffic.variables && typeof matchedTraffic.variables[variableKey] !== "undefined") {
|
|
216
|
+
logger.debug("using variable from rule", {
|
|
217
|
+
featureKey: feature.key,
|
|
218
|
+
variableKey,
|
|
219
|
+
bucketValue,
|
|
220
|
+
});
|
|
221
|
+
|
|
177
222
|
return matchedTraffic.variables[variableKey];
|
|
178
223
|
}
|
|
179
224
|
|
|
180
225
|
const allocation = getMatchedAllocation(matchedTraffic, bucketValue);
|
|
181
226
|
|
|
182
227
|
if (!allocation) {
|
|
228
|
+
logger.debug("no matched allocation found", {
|
|
229
|
+
featureKey: feature.key,
|
|
230
|
+
variableKey,
|
|
231
|
+
bucketValue,
|
|
232
|
+
});
|
|
233
|
+
|
|
183
234
|
return undefined;
|
|
184
235
|
}
|
|
185
236
|
|
|
@@ -190,6 +241,14 @@ export function getBucketedVariableValue(
|
|
|
190
241
|
});
|
|
191
242
|
|
|
192
243
|
if (!variation) {
|
|
244
|
+
// this should never happen
|
|
245
|
+
logger.debug("no matched variation found", {
|
|
246
|
+
feature: feature.key,
|
|
247
|
+
variableKey,
|
|
248
|
+
variation: variationValue,
|
|
249
|
+
bucketValue,
|
|
250
|
+
});
|
|
251
|
+
|
|
193
252
|
return undefined;
|
|
194
253
|
}
|
|
195
254
|
|
|
@@ -198,6 +257,13 @@ export function getBucketedVariableValue(
|
|
|
198
257
|
});
|
|
199
258
|
|
|
200
259
|
if (!variableFromVariation) {
|
|
260
|
+
logger.debug("using default value as variation has no variable", {
|
|
261
|
+
featureKey: feature.key,
|
|
262
|
+
variableKey,
|
|
263
|
+
variation: variationValue,
|
|
264
|
+
bucketValue,
|
|
265
|
+
});
|
|
266
|
+
|
|
201
267
|
if (variableSchema.type === "json") {
|
|
202
268
|
return JSON.parse(variableSchema.defaultValue as string);
|
|
203
269
|
}
|
|
@@ -228,6 +294,13 @@ export function getBucketedVariableValue(
|
|
|
228
294
|
});
|
|
229
295
|
|
|
230
296
|
if (override) {
|
|
297
|
+
logger.debug("using override value from variation", {
|
|
298
|
+
feature: feature.key,
|
|
299
|
+
variableKey,
|
|
300
|
+
variation: variationValue,
|
|
301
|
+
bucketValue,
|
|
302
|
+
});
|
|
303
|
+
|
|
231
304
|
if (variableSchema.type === "json") {
|
|
232
305
|
return JSON.parse(override.value as string);
|
|
233
306
|
}
|
|
@@ -236,6 +309,13 @@ export function getBucketedVariableValue(
|
|
|
236
309
|
}
|
|
237
310
|
}
|
|
238
311
|
|
|
312
|
+
logger.debug("using value from variation", {
|
|
313
|
+
feature: feature.key,
|
|
314
|
+
variableKey,
|
|
315
|
+
variation: variationValue,
|
|
316
|
+
bucketValue,
|
|
317
|
+
});
|
|
318
|
+
|
|
239
319
|
if (variableSchema.type === "json") {
|
|
240
320
|
return JSON.parse(variableFromVariation.value as string);
|
|
241
321
|
}
|
package/src/index.ts
CHANGED
package/src/logger.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
2
|
+
|
|
3
|
+
export type LogMessage = string;
|
|
4
|
+
|
|
5
|
+
export interface LogDetails {
|
|
6
|
+
[key: string]: any;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type LogHandler = (level: LogLevel, message: LogMessage, details?: LogDetails) => void;
|
|
10
|
+
|
|
11
|
+
export interface CreateLoggerOptions {
|
|
12
|
+
levels?: LogLevel[];
|
|
13
|
+
handler?: LogHandler;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const loggerPrefix = "[Featurevisor]";
|
|
17
|
+
|
|
18
|
+
export const defaultLogLevels: LogLevel[] = [
|
|
19
|
+
// supported, but not enabled by default
|
|
20
|
+
// "debug",
|
|
21
|
+
// "info",
|
|
22
|
+
|
|
23
|
+
// enabled by default
|
|
24
|
+
"warn",
|
|
25
|
+
"error",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export const defaultLogHandler: LogHandler = function defaultLogHandler(
|
|
29
|
+
level,
|
|
30
|
+
message,
|
|
31
|
+
details = {},
|
|
32
|
+
) {
|
|
33
|
+
switch (level) {
|
|
34
|
+
case "debug":
|
|
35
|
+
console.log(loggerPrefix, message, details);
|
|
36
|
+
case "info":
|
|
37
|
+
console.info(loggerPrefix, message, details);
|
|
38
|
+
case "warn":
|
|
39
|
+
console.warn(loggerPrefix, message, details);
|
|
40
|
+
case "error":
|
|
41
|
+
console.error(loggerPrefix, message, details);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export class Logger {
|
|
46
|
+
private levels: LogLevel[];
|
|
47
|
+
private handle: LogHandler;
|
|
48
|
+
|
|
49
|
+
constructor(options: CreateLoggerOptions) {
|
|
50
|
+
this.levels = options.levels as LogLevel[];
|
|
51
|
+
this.handle = options.handler as LogHandler;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setLevels(levels: LogLevel[]) {
|
|
55
|
+
this.levels = levels;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
log(level: LogLevel, message: LogMessage, details?: LogDetails) {
|
|
59
|
+
if (this.levels.indexOf(level) !== -1) {
|
|
60
|
+
this.handle(level, message, details);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
debug(message: LogMessage, details?: LogDetails) {
|
|
65
|
+
this.log("debug", message, details);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
info(message: LogMessage, details?: LogDetails) {
|
|
69
|
+
this.log("info", message, details);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
warn(message: LogMessage, details?: LogDetails) {
|
|
73
|
+
this.log("warn", message, details);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
error(message: LogMessage, details?: LogDetails) {
|
|
77
|
+
this.log("error", message, details);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function createLogger(options: CreateLoggerOptions = {}): Logger {
|
|
82
|
+
const levels = options.levels || defaultLogLevels;
|
|
83
|
+
const logHandler = options.handler || defaultLogHandler;
|
|
84
|
+
|
|
85
|
+
return new Logger({ levels, handler: logHandler });
|
|
86
|
+
}
|