@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/src/client.ts CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  getForcedVariableValue,
19
19
  } from "./feature";
20
20
  import { getBucketedNumber } from "./bucket";
21
- import { VariableSchema } from "@featurevisor/types/src";
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
- console.error(`Featurevisor could not parse the datafile`);
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(feature, attributes, bucketValue, this.datafileReader);
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
- console.error("[Featurevisor]", e);
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 (!variationValue) {
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
- console.error("[Featurevisor]", e);
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
- console.error("[Featurevisor]", e);
332
+ this.logger.error("getVariable", { featureKey, variableKey, error: e });
304
333
 
305
334
  return undefined;
306
335
  }
@@ -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` or `datafileUrl` option",
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
@@ -1,3 +1,4 @@
1
1
  export * from "./bucket";
2
2
  export * from "./client";
3
3
  export * from "./createInstance";
4
+ export * from "./logger";
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
+ }