@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
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { DatafileReader } from "./datafileReader";
|
|
2
|
+
import { createLogger } from "./logger";
|
|
3
|
+
|
|
4
|
+
import { getParamsForDatafileSetEvent, getParamsForStickySetEvent } from "./events";
|
|
5
|
+
|
|
6
|
+
describe("sdk: events", function () {
|
|
7
|
+
describe("getParamsForStickySetEvent", function () {
|
|
8
|
+
it("should get params for sticky set event: empty to new", function () {
|
|
9
|
+
const previousStickyFeatures = {};
|
|
10
|
+
const newStickyFeatures = {
|
|
11
|
+
feature2: { enabled: true },
|
|
12
|
+
feature3: { enabled: true },
|
|
13
|
+
};
|
|
14
|
+
const replace = true;
|
|
15
|
+
|
|
16
|
+
const result = getParamsForStickySetEvent(previousStickyFeatures, newStickyFeatures, replace);
|
|
17
|
+
|
|
18
|
+
expect(result).toEqual({
|
|
19
|
+
features: ["feature2", "feature3"],
|
|
20
|
+
replaced: replace,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should get params for sticky set event: add, change, remove", function () {
|
|
25
|
+
const previousStickyFeatures = {
|
|
26
|
+
feature1: { enabled: true },
|
|
27
|
+
feature2: { enabled: true },
|
|
28
|
+
};
|
|
29
|
+
const newStickyFeatures = {
|
|
30
|
+
feature2: { enabled: true },
|
|
31
|
+
feature3: { enabled: true },
|
|
32
|
+
};
|
|
33
|
+
const replace = true;
|
|
34
|
+
|
|
35
|
+
const result = getParamsForStickySetEvent(previousStickyFeatures, newStickyFeatures, replace);
|
|
36
|
+
|
|
37
|
+
expect(result).toEqual({
|
|
38
|
+
features: ["feature1", "feature2", "feature3"],
|
|
39
|
+
replaced: replace,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("getParamsForDatafileSetEvent", function () {
|
|
45
|
+
const logger = createLogger({
|
|
46
|
+
level: "error",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should get params for datafile set event: empty to new", function () {
|
|
50
|
+
const previousDatafileReader = new DatafileReader({
|
|
51
|
+
datafile: {
|
|
52
|
+
schemaVersion: "1.0.0",
|
|
53
|
+
revision: "1",
|
|
54
|
+
features: {},
|
|
55
|
+
segments: {},
|
|
56
|
+
},
|
|
57
|
+
logger,
|
|
58
|
+
});
|
|
59
|
+
const newDatafileReader = new DatafileReader({
|
|
60
|
+
datafile: {
|
|
61
|
+
schemaVersion: "1.0.0",
|
|
62
|
+
revision: "2",
|
|
63
|
+
features: {
|
|
64
|
+
feature1: { bucketBy: "userId", hash: "hash1", traffic: [] },
|
|
65
|
+
feature2: { bucketBy: "userId", hash: "hash2", traffic: [] },
|
|
66
|
+
},
|
|
67
|
+
segments: {},
|
|
68
|
+
},
|
|
69
|
+
logger,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const result = getParamsForDatafileSetEvent(previousDatafileReader, newDatafileReader);
|
|
73
|
+
|
|
74
|
+
expect(result).toEqual({
|
|
75
|
+
revision: "2",
|
|
76
|
+
previousRevision: "1",
|
|
77
|
+
revisionChanged: true,
|
|
78
|
+
features: ["feature1", "feature2"],
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should get params for datafile set event: change hash, addition", function () {
|
|
83
|
+
const previousDatafileReader = new DatafileReader({
|
|
84
|
+
datafile: {
|
|
85
|
+
schemaVersion: "1.0.0",
|
|
86
|
+
revision: "1",
|
|
87
|
+
features: {
|
|
88
|
+
feature1: { bucketBy: "userId", hash: "hash-same", traffic: [] },
|
|
89
|
+
feature2: { bucketBy: "userId", hash: "hash1-2", traffic: [] },
|
|
90
|
+
},
|
|
91
|
+
segments: {},
|
|
92
|
+
},
|
|
93
|
+
logger,
|
|
94
|
+
});
|
|
95
|
+
const newDatafileReader = new DatafileReader({
|
|
96
|
+
datafile: {
|
|
97
|
+
schemaVersion: "1.0.0",
|
|
98
|
+
revision: "2",
|
|
99
|
+
features: {
|
|
100
|
+
feature1: { bucketBy: "userId", hash: "hash-same", traffic: [] },
|
|
101
|
+
feature2: { bucketBy: "userId", hash: "hash2-2", traffic: [] },
|
|
102
|
+
feature3: { bucketBy: "userId", hash: "hash2-3", traffic: [] },
|
|
103
|
+
},
|
|
104
|
+
segments: {},
|
|
105
|
+
},
|
|
106
|
+
logger,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const result = getParamsForDatafileSetEvent(previousDatafileReader, newDatafileReader);
|
|
110
|
+
|
|
111
|
+
expect(result).toEqual({
|
|
112
|
+
revision: "2",
|
|
113
|
+
previousRevision: "1",
|
|
114
|
+
revisionChanged: true,
|
|
115
|
+
features: ["feature2", "feature3"],
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should get params for datafile set event: change hash, removal", function () {
|
|
120
|
+
const previousDatafileReader = new DatafileReader({
|
|
121
|
+
datafile: {
|
|
122
|
+
schemaVersion: "1.0.0",
|
|
123
|
+
revision: "1",
|
|
124
|
+
features: {
|
|
125
|
+
feature1: { bucketBy: "userId", hash: "hash-same", traffic: [] },
|
|
126
|
+
feature2: { bucketBy: "userId", hash: "hash1-2", traffic: [] },
|
|
127
|
+
},
|
|
128
|
+
segments: {},
|
|
129
|
+
},
|
|
130
|
+
logger,
|
|
131
|
+
});
|
|
132
|
+
const newDatafileReader = new DatafileReader({
|
|
133
|
+
datafile: {
|
|
134
|
+
schemaVersion: "1.0.0",
|
|
135
|
+
revision: "2",
|
|
136
|
+
features: {
|
|
137
|
+
feature2: { bucketBy: "userId", hash: "hash2-2", traffic: [] },
|
|
138
|
+
},
|
|
139
|
+
segments: {},
|
|
140
|
+
},
|
|
141
|
+
logger,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const result = getParamsForDatafileSetEvent(previousDatafileReader, newDatafileReader);
|
|
145
|
+
|
|
146
|
+
expect(result).toEqual({
|
|
147
|
+
revision: "2",
|
|
148
|
+
previousRevision: "1",
|
|
149
|
+
revisionChanged: true,
|
|
150
|
+
features: ["feature1", "feature2"],
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { StickyFeatures, FeatureKey } from "@featurevisor/types";
|
|
2
|
+
|
|
3
|
+
import type { EventDetails } from "./emitter";
|
|
4
|
+
import type { DatafileReader } from "./datafileReader";
|
|
5
|
+
|
|
6
|
+
export function getParamsForStickySetEvent(
|
|
7
|
+
previousStickyFeatures: StickyFeatures = {},
|
|
8
|
+
newStickyFeatures: StickyFeatures = {},
|
|
9
|
+
replace,
|
|
10
|
+
): EventDetails {
|
|
11
|
+
const keysBefore = Object.keys(previousStickyFeatures);
|
|
12
|
+
const keysAfter = Object.keys(newStickyFeatures);
|
|
13
|
+
|
|
14
|
+
const allKeys = [...keysBefore, ...keysAfter];
|
|
15
|
+
const uniqueFeaturesAffected = allKeys.filter(
|
|
16
|
+
(element, index) => allKeys.indexOf(element) === index,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
features: uniqueFeaturesAffected,
|
|
21
|
+
replaced: replace,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getParamsForDatafileSetEvent(
|
|
26
|
+
previousDatafileReader: DatafileReader,
|
|
27
|
+
newDatafileReader: DatafileReader,
|
|
28
|
+
): EventDetails {
|
|
29
|
+
const previousRevision = previousDatafileReader.getRevision();
|
|
30
|
+
const previousFeatureKeys = previousDatafileReader.getFeatureKeys();
|
|
31
|
+
|
|
32
|
+
const newRevision = newDatafileReader.getRevision();
|
|
33
|
+
const newFeatureKeys = newDatafileReader.getFeatureKeys();
|
|
34
|
+
|
|
35
|
+
// results
|
|
36
|
+
const removedFeatures: FeatureKey[] = [];
|
|
37
|
+
const changedFeatures: FeatureKey[] = [];
|
|
38
|
+
const addedFeatures: FeatureKey[] = [];
|
|
39
|
+
|
|
40
|
+
// checking against existing datafile
|
|
41
|
+
for (const previousFeatureKey of previousFeatureKeys) {
|
|
42
|
+
if (newFeatureKeys.indexOf(previousFeatureKey) === -1) {
|
|
43
|
+
// feature was removed in new datafile
|
|
44
|
+
removedFeatures.push(previousFeatureKey);
|
|
45
|
+
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// feature exists in both datafiles, check if it was changed
|
|
50
|
+
const previousFeature = previousDatafileReader.getFeature(previousFeatureKey);
|
|
51
|
+
const newFeature = newDatafileReader.getFeature(previousFeatureKey);
|
|
52
|
+
|
|
53
|
+
if (previousFeature?.hash !== newFeature?.hash) {
|
|
54
|
+
// feature was changed in new datafile
|
|
55
|
+
changedFeatures.push(previousFeatureKey);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// checking against new datafile
|
|
60
|
+
for (const newFeatureKey of newFeatureKeys) {
|
|
61
|
+
if (previousFeatureKeys.indexOf(newFeatureKey) === -1) {
|
|
62
|
+
// feature was added in new datafile
|
|
63
|
+
addedFeatures.push(newFeatureKey);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// combine all affected feature keys
|
|
68
|
+
const allAffectedFeatures: FeatureKey[] = [
|
|
69
|
+
...removedFeatures,
|
|
70
|
+
...changedFeatures,
|
|
71
|
+
...addedFeatures,
|
|
72
|
+
].filter((element, index, array) => array.indexOf(element) === index);
|
|
73
|
+
|
|
74
|
+
const details = {
|
|
75
|
+
revision: newRevision,
|
|
76
|
+
previousRevision,
|
|
77
|
+
revisionChanged: previousRevision !== newRevision,
|
|
78
|
+
|
|
79
|
+
features: allAffectedFeatures,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return details;
|
|
83
|
+
}
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { VariableType, VariableValue } from "@featurevisor/types";
|
|
2
|
+
|
|
3
|
+
type FieldType = string | VariableType;
|
|
4
|
+
type ValueType = VariableValue;
|
|
5
|
+
|
|
6
|
+
export function getValueByType(value: ValueType, fieldType: FieldType): ValueType {
|
|
7
|
+
try {
|
|
8
|
+
if (value === undefined) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
switch (fieldType) {
|
|
13
|
+
case "string":
|
|
14
|
+
return typeof value === "string" ? value : null;
|
|
15
|
+
case "integer":
|
|
16
|
+
return parseInt(value as string, 10);
|
|
17
|
+
case "double":
|
|
18
|
+
return parseFloat(value as string);
|
|
19
|
+
case "boolean":
|
|
20
|
+
return value === true;
|
|
21
|
+
case "array":
|
|
22
|
+
return Array.isArray(value) ? value : null;
|
|
23
|
+
case "object":
|
|
24
|
+
return typeof value === "object" ? value : null;
|
|
25
|
+
// @NOTE: `json` is not handled here intentionally
|
|
26
|
+
default:
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
// eslint-disable-next-line
|
|
30
|
+
} catch (e) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { BucketBy, BucketKey, BucketValue, Context, FeatureKey } from "@featurevisor/types";
|
|
2
|
+
|
|
3
|
+
import type { EvaluateOptions, Evaluation } from "./evaluate";
|
|
4
|
+
import type { Logger } from "./logger";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* bucketKey
|
|
8
|
+
*/
|
|
9
|
+
export interface ConfigureBucketKeyOptions {
|
|
10
|
+
featureKey: FeatureKey;
|
|
11
|
+
context: Context;
|
|
12
|
+
bucketBy: BucketBy;
|
|
13
|
+
bucketKey: string; // the initial bucket key, which can be modified by hooks
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ConfigureBucketKey = (options: ConfigureBucketKeyOptions) => BucketKey;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* bucketValue
|
|
20
|
+
*/
|
|
21
|
+
export interface ConfigureBucketValueOptions {
|
|
22
|
+
featureKey: FeatureKey;
|
|
23
|
+
bucketKey: string;
|
|
24
|
+
context: Context;
|
|
25
|
+
bucketValue: number; // the initial bucket value, which can be modified by hooks
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ConfigureBucketValue = (options: ConfigureBucketValueOptions) => BucketValue;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Hooks
|
|
32
|
+
*/
|
|
33
|
+
export interface Hook {
|
|
34
|
+
name: string;
|
|
35
|
+
|
|
36
|
+
before?: (options: EvaluateOptions) => EvaluateOptions;
|
|
37
|
+
|
|
38
|
+
bucketKey?: ConfigureBucketKey;
|
|
39
|
+
|
|
40
|
+
bucketValue?: ConfigureBucketValue;
|
|
41
|
+
|
|
42
|
+
after?: (evaluation: Evaluation, options: EvaluateOptions) => Evaluation;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface HooksManagerOptions {
|
|
46
|
+
hooks?: Hook[];
|
|
47
|
+
logger: Logger;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class HooksManager {
|
|
51
|
+
private hooks: Hook[] = [];
|
|
52
|
+
private logger: Logger;
|
|
53
|
+
|
|
54
|
+
constructor(options: HooksManagerOptions) {
|
|
55
|
+
this.logger = options.logger;
|
|
56
|
+
|
|
57
|
+
if (options.hooks) {
|
|
58
|
+
options.hooks.forEach((hook) => {
|
|
59
|
+
this.add(hook);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
add(hook: Hook): (() => void) | undefined {
|
|
65
|
+
if (this.hooks.some((existingHook) => existingHook.name === hook.name)) {
|
|
66
|
+
this.logger.error(`Hook with name "${hook.name}" already exists.`, {
|
|
67
|
+
name: hook.name,
|
|
68
|
+
hook: hook,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.hooks.push(hook);
|
|
75
|
+
|
|
76
|
+
return () => {
|
|
77
|
+
this.remove(hook.name);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
remove(name: string): void {
|
|
82
|
+
this.hooks = this.hooks.filter((hook) => hook.name !== name);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getAll(): Hook[] {
|
|
86
|
+
return this.hooks;
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export * from "./
|
|
1
|
+
export * from "./bucketer";
|
|
2
2
|
export * from "./instance";
|
|
3
3
|
export * from "./logger";
|
|
4
4
|
export * from "./conditions";
|
|
5
|
-
export * from "./emitter";
|
|
6
5
|
export * from "./evaluate";
|
|
6
|
+
export * from "./datafileReader";
|
|
7
|
+
export * from "./child";
|