@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/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,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
- console.error(`Featurevisor could not parse the datafile`);
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 forcedVariation = getForcedVariation(feature, attributes, this.datafileReader);
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, attributes);
167
+ const bucketValue = this.getBucketValue(feature, finalAttributes);
148
168
 
149
- const variation = getBucketedVariation(feature, attributes, bucketValue, this.datafileReader);
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
- console.error("[Featurevisor]", e);
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 (!variationValue) {
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 attributes[a.key] !== "undefined") {
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, attributes, captureAttributes);
259
+ this.onActivation(featureKey, variationValue, finalAttributes, captureAttributes);
224
260
  }
225
261
 
226
262
  return variationValue;
227
263
  } catch (e) {
228
- console.error("[Featurevisor]", e);
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
- attributes,
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, attributes);
339
+ const bucketValue = this.getBucketValue(feature, finalAttributes);
294
340
 
295
341
  return getBucketedVariableValue(
296
342
  feature,
297
343
  variableSchema,
298
- attributes,
344
+ finalAttributes,
299
345
  bucketValue,
300
346
  this.datafileReader,
347
+ this.logger,
301
348
  );
302
349
  } catch (e) {
303
- console.error("[Featurevisor]", e);
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
+ });
@@ -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` or `datafileUrl` option",
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
@@ -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
+ }