@featurevisor/sdk 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/index.js +1 -1
- package/dist/index.js.gz +0 -0
- package/dist/index.js.map +1 -1
- package/lib/client.d.ts +5 -0
- package/lib/client.js +40 -14
- package/lib/client.js.map +1 -1
- package/lib/createInstance.d.ts +4 -1
- package/lib/createInstance.js +7 -1
- package/lib/createInstance.js.map +1 -1
- package/lib/createInstance.spec.d.ts +1 -0
- 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 +62 -15
- package/src/createInstance.spec.ts +70 -0
- package/src/createInstance.ts +11 -2
- 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,8 @@ 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
|
|
37
|
+
interceptAttributes?: (attributes: Attributes) => Attributes; // @TODO: move it to FeaturevisorInstance in next breaking semver
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
type FieldType = VariationType | VariableType;
|
|
@@ -68,6 +70,8 @@ export class FeaturevisorSDK {
|
|
|
68
70
|
private onActivation?: ActivationCallback;
|
|
69
71
|
private datafileReader: DatafileReader;
|
|
70
72
|
private configureBucketValue?: ConfigureBucketValue;
|
|
73
|
+
private logger: Logger;
|
|
74
|
+
private interceptAttributes?: (attributes: Attributes) => Attributes;
|
|
71
75
|
|
|
72
76
|
constructor(options: SdkOptions) {
|
|
73
77
|
if (options.onActivation) {
|
|
@@ -78,6 +82,12 @@ export class FeaturevisorSDK {
|
|
|
78
82
|
this.configureBucketValue = options.configureBucketValue;
|
|
79
83
|
}
|
|
80
84
|
|
|
85
|
+
this.logger = options.logger || createLogger();
|
|
86
|
+
|
|
87
|
+
if (options.interceptAttributes) {
|
|
88
|
+
this.interceptAttributes = options.interceptAttributes;
|
|
89
|
+
}
|
|
90
|
+
|
|
81
91
|
this.setDatafile(options.datafile);
|
|
82
92
|
}
|
|
83
93
|
|
|
@@ -87,8 +97,7 @@ export class FeaturevisorSDK {
|
|
|
87
97
|
typeof datafile === "string" ? JSON.parse(datafile) : datafile,
|
|
88
98
|
);
|
|
89
99
|
} catch (e) {
|
|
90
|
-
|
|
91
|
-
console.error(e);
|
|
100
|
+
this.logger.error("could not parse datafile", { error: e });
|
|
92
101
|
}
|
|
93
102
|
}
|
|
94
103
|
|
|
@@ -135,26 +144,49 @@ export class FeaturevisorSDK {
|
|
|
135
144
|
const feature = this.getFeature(featureKey);
|
|
136
145
|
|
|
137
146
|
if (!feature) {
|
|
147
|
+
this.logger.warn("feature not found in datafile", { featureKey });
|
|
148
|
+
|
|
138
149
|
return undefined;
|
|
139
150
|
}
|
|
140
151
|
|
|
141
|
-
const
|
|
152
|
+
const finalAttributes = this.interceptAttributes
|
|
153
|
+
? this.interceptAttributes(attributes)
|
|
154
|
+
: attributes;
|
|
155
|
+
|
|
156
|
+
const forcedVariation = getForcedVariation(feature, finalAttributes, this.datafileReader);
|
|
142
157
|
|
|
143
158
|
if (forcedVariation) {
|
|
159
|
+
this.logger.debug("forced variation found", {
|
|
160
|
+
featureKey,
|
|
161
|
+
variation: forcedVariation.value,
|
|
162
|
+
});
|
|
163
|
+
|
|
144
164
|
return forcedVariation.value;
|
|
145
165
|
}
|
|
146
166
|
|
|
147
|
-
const bucketValue = this.getBucketValue(feature,
|
|
167
|
+
const bucketValue = this.getBucketValue(feature, finalAttributes);
|
|
148
168
|
|
|
149
|
-
const variation = getBucketedVariation(
|
|
169
|
+
const variation = getBucketedVariation(
|
|
170
|
+
feature,
|
|
171
|
+
finalAttributes,
|
|
172
|
+
bucketValue,
|
|
173
|
+
this.datafileReader,
|
|
174
|
+
this.logger,
|
|
175
|
+
);
|
|
150
176
|
|
|
151
177
|
if (!variation) {
|
|
178
|
+
this.logger.debug("using default variation", {
|
|
179
|
+
featureKey,
|
|
180
|
+
bucketValue,
|
|
181
|
+
variation: feature.defaultVariation,
|
|
182
|
+
});
|
|
183
|
+
|
|
152
184
|
return feature.defaultVariation;
|
|
153
185
|
}
|
|
154
186
|
|
|
155
187
|
return variation.value;
|
|
156
188
|
} catch (e) {
|
|
157
|
-
|
|
189
|
+
this.logger.error("getVariation", { featureKey, error: e });
|
|
158
190
|
|
|
159
191
|
return undefined;
|
|
160
192
|
}
|
|
@@ -203,11 +235,15 @@ export class FeaturevisorSDK {
|
|
|
203
235
|
try {
|
|
204
236
|
const variationValue = this.getVariation(featureKey, attributes);
|
|
205
237
|
|
|
206
|
-
if (
|
|
238
|
+
if (typeof variationValue === "undefined") {
|
|
207
239
|
return undefined;
|
|
208
240
|
}
|
|
209
241
|
|
|
210
242
|
if (this.onActivation) {
|
|
243
|
+
const finalAttributes = this.interceptAttributes
|
|
244
|
+
? this.interceptAttributes(attributes)
|
|
245
|
+
: attributes;
|
|
246
|
+
|
|
211
247
|
const captureAttributes: Attributes = {};
|
|
212
248
|
|
|
213
249
|
const attributesForCapturing = this.datafileReader
|
|
@@ -215,17 +251,17 @@ export class FeaturevisorSDK {
|
|
|
215
251
|
.filter((a) => a.capture === true);
|
|
216
252
|
|
|
217
253
|
attributesForCapturing.forEach((a) => {
|
|
218
|
-
if (typeof
|
|
254
|
+
if (typeof finalAttributes[a.key] !== "undefined") {
|
|
219
255
|
captureAttributes[a.key] = attributes[a.key];
|
|
220
256
|
}
|
|
221
257
|
});
|
|
222
258
|
|
|
223
|
-
this.onActivation(featureKey, variationValue,
|
|
259
|
+
this.onActivation(featureKey, variationValue, finalAttributes, captureAttributes);
|
|
224
260
|
}
|
|
225
261
|
|
|
226
262
|
return variationValue;
|
|
227
263
|
} catch (e) {
|
|
228
|
-
|
|
264
|
+
this.logger.error("activate", { featureKey, error: e });
|
|
229
265
|
|
|
230
266
|
return undefined;
|
|
231
267
|
}
|
|
@@ -268,6 +304,8 @@ export class FeaturevisorSDK {
|
|
|
268
304
|
const feature = this.getFeature(featureKey);
|
|
269
305
|
|
|
270
306
|
if (!feature) {
|
|
307
|
+
this.logger.warn("feature not found in datafile", { featureKey, variableKey });
|
|
308
|
+
|
|
271
309
|
return undefined;
|
|
272
310
|
}
|
|
273
311
|
|
|
@@ -276,31 +314,40 @@ export class FeaturevisorSDK {
|
|
|
276
314
|
: undefined;
|
|
277
315
|
|
|
278
316
|
if (!variableSchema) {
|
|
317
|
+
this.logger.warn("variable schema not found", { featureKey, variableKey });
|
|
318
|
+
|
|
279
319
|
return undefined;
|
|
280
320
|
}
|
|
281
321
|
|
|
322
|
+
const finalAttributes = this.interceptAttributes
|
|
323
|
+
? this.interceptAttributes(attributes)
|
|
324
|
+
: attributes;
|
|
325
|
+
|
|
282
326
|
const forcedVariableValue = getForcedVariableValue(
|
|
283
327
|
feature,
|
|
284
328
|
variableSchema,
|
|
285
|
-
|
|
329
|
+
finalAttributes,
|
|
286
330
|
this.datafileReader,
|
|
287
331
|
);
|
|
288
332
|
|
|
289
333
|
if (typeof forcedVariableValue !== "undefined") {
|
|
334
|
+
this.logger.debug("forced variable value found", { featureKey, variableKey });
|
|
335
|
+
|
|
290
336
|
return forcedVariableValue;
|
|
291
337
|
}
|
|
292
338
|
|
|
293
|
-
const bucketValue = this.getBucketValue(feature,
|
|
339
|
+
const bucketValue = this.getBucketValue(feature, finalAttributes);
|
|
294
340
|
|
|
295
341
|
return getBucketedVariableValue(
|
|
296
342
|
feature,
|
|
297
343
|
variableSchema,
|
|
298
|
-
|
|
344
|
+
finalAttributes,
|
|
299
345
|
bucketValue,
|
|
300
346
|
this.datafileReader,
|
|
347
|
+
this.logger,
|
|
301
348
|
);
|
|
302
349
|
} catch (e) {
|
|
303
|
-
|
|
350
|
+
this.logger.error("getVariable", { featureKey, variableKey, error: e });
|
|
304
351
|
|
|
305
352
|
return undefined;
|
|
306
353
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createInstance } from "./createInstance";
|
|
2
|
+
|
|
3
|
+
describe("sdk: createInstance", function () {
|
|
4
|
+
it("should be a function", function () {
|
|
5
|
+
expect(typeof createInstance).toEqual("function");
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it("should create instance with datafile content", function () {
|
|
9
|
+
const sdk = createInstance({
|
|
10
|
+
datafile: {
|
|
11
|
+
schemaVersion: "1",
|
|
12
|
+
revision: "1.0",
|
|
13
|
+
features: [],
|
|
14
|
+
attributes: [],
|
|
15
|
+
segments: [],
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(typeof sdk.getVariation).toEqual("function");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should intercept attributes", function () {
|
|
23
|
+
let intercepted = false;
|
|
24
|
+
|
|
25
|
+
const sdk = createInstance({
|
|
26
|
+
datafile: {
|
|
27
|
+
schemaVersion: "1",
|
|
28
|
+
revision: "1.0",
|
|
29
|
+
features: [
|
|
30
|
+
{
|
|
31
|
+
key: "test",
|
|
32
|
+
defaultVariation: false,
|
|
33
|
+
bucketBy: "userId",
|
|
34
|
+
variations: [
|
|
35
|
+
{ type: "boolean", value: true },
|
|
36
|
+
{ type: "boolean", value: false },
|
|
37
|
+
],
|
|
38
|
+
traffic: [
|
|
39
|
+
{
|
|
40
|
+
key: "1",
|
|
41
|
+
segments: "*",
|
|
42
|
+
percentage: 100000,
|
|
43
|
+
allocation: [
|
|
44
|
+
{ variation: true, percentage: 100000 },
|
|
45
|
+
{ variation: false, percentage: 0 },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
attributes: [],
|
|
52
|
+
segments: [],
|
|
53
|
+
},
|
|
54
|
+
interceptAttributes: function (attributes) {
|
|
55
|
+
intercepted = true;
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
...attributes,
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const variation = sdk.getVariation("test", {
|
|
64
|
+
userId: "123",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(variation).toEqual(true);
|
|
68
|
+
expect(intercepted).toEqual(true);
|
|
69
|
+
});
|
|
70
|
+
});
|
package/src/createInstance.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { DatafileContent } from "@featurevisor/types";
|
|
1
|
+
import { DatafileContent, Attributes } 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,8 @@ export interface InstanceOptions {
|
|
|
13
14
|
datafileUrl?: string;
|
|
14
15
|
onReady?: ReadyCallback;
|
|
15
16
|
handleDatafileFetch?: (datafileUrl: string) => Promise<DatafileContent>;
|
|
17
|
+
logger?: Logger;
|
|
18
|
+
interceptAttributes?: (attributes: Attributes) => Attributes;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
// @TODO: consider renaming it to FeaturevisorSDK in next breaking semver
|
|
@@ -106,16 +109,20 @@ function fetchDatafileContent(datafileUrl, options: InstanceOptions): Promise<Da
|
|
|
106
109
|
export function createInstance(options: InstanceOptions) {
|
|
107
110
|
if (!options.datafile && !options.datafileUrl) {
|
|
108
111
|
throw new Error(
|
|
109
|
-
"Featurevisor SDK instance cannot be created without `datafile`
|
|
112
|
+
"Featurevisor SDK instance cannot be created without both `datafile` and `datafileUrl` options",
|
|
110
113
|
);
|
|
111
114
|
}
|
|
112
115
|
|
|
116
|
+
const logger = options.logger || createLogger();
|
|
117
|
+
|
|
113
118
|
// datafile content is already provided
|
|
114
119
|
if (options.datafile) {
|
|
115
120
|
const sdk = new FeaturevisorSDK({
|
|
116
121
|
datafile: options.datafile,
|
|
117
122
|
onActivation: options.onActivation,
|
|
118
123
|
configureBucketValue: options.configureBucketValue,
|
|
124
|
+
logger,
|
|
125
|
+
interceptAttributes: options.interceptAttributes,
|
|
119
126
|
});
|
|
120
127
|
|
|
121
128
|
if (typeof options.onReady === "function") {
|
|
@@ -134,6 +141,8 @@ export function createInstance(options: InstanceOptions) {
|
|
|
134
141
|
datafile: emptyDatafile,
|
|
135
142
|
onActivation: options.onActivation,
|
|
136
143
|
configureBucketValue: options.configureBucketValue,
|
|
144
|
+
logger,
|
|
145
|
+
interceptAttributes: options.interceptAttributes,
|
|
137
146
|
});
|
|
138
147
|
|
|
139
148
|
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
|
+
}
|