@featurevisor/sdk 1.35.2 → 2.0.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +2 -381
  3. package/coverage/clover.xml +707 -643
  4. package/coverage/coverage-final.json +11 -9
  5. package/coverage/lcov-report/{segments.ts.html → bucketer.ts.html} +155 -77
  6. package/coverage/lcov-report/child.ts.html +940 -0
  7. package/coverage/lcov-report/conditions.ts.html +107 -158
  8. package/coverage/lcov-report/datafileReader.ts.html +763 -103
  9. package/coverage/lcov-report/emitter.ts.html +77 -59
  10. package/coverage/lcov-report/evaluate.ts.html +689 -416
  11. package/coverage/lcov-report/events.ts.html +334 -0
  12. package/coverage/lcov-report/helpers.ts.html +184 -0
  13. package/coverage/lcov-report/{feature.ts.html → hooks.ts.html} +90 -237
  14. package/coverage/lcov-report/index.html +119 -89
  15. package/coverage/lcov-report/instance.ts.html +341 -773
  16. package/coverage/lcov-report/logger.ts.html +64 -64
  17. package/coverage/lcov.info +1433 -1223
  18. package/dist/bucketer.d.ts +11 -0
  19. package/dist/child.d.ts +26 -0
  20. package/dist/compareVersions.d.ts +4 -0
  21. package/dist/conditions.d.ts +4 -4
  22. package/dist/datafileReader.d.ts +26 -6
  23. package/dist/emitter.d.ts +8 -9
  24. package/dist/evaluate.d.ts +31 -29
  25. package/dist/events.d.ts +5 -0
  26. package/dist/helpers.d.ts +5 -0
  27. package/dist/hooks.d.ts +45 -0
  28. package/dist/index.d.ts +3 -2
  29. package/dist/index.js +1 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.mjs +1 -1
  32. package/dist/index.mjs.gz +0 -0
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/instance.d.ts +40 -72
  35. package/dist/logger.d.ts +6 -5
  36. package/dist/murmurhash.d.ts +1 -0
  37. package/jest.config.js +2 -0
  38. package/lib/bucketer.d.ts +11 -0
  39. package/lib/child.d.ts +26 -0
  40. package/lib/compareVersions.d.ts +4 -0
  41. package/lib/conditions.d.ts +4 -4
  42. package/lib/datafileReader.d.ts +26 -6
  43. package/lib/emitter.d.ts +8 -9
  44. package/lib/evaluate.d.ts +31 -29
  45. package/lib/events.d.ts +5 -0
  46. package/lib/helpers.d.ts +5 -0
  47. package/lib/hooks.d.ts +45 -0
  48. package/lib/index.d.ts +3 -2
  49. package/lib/instance.d.ts +40 -72
  50. package/lib/logger.d.ts +6 -5
  51. package/lib/murmurhash.d.ts +1 -0
  52. package/package.json +3 -5
  53. package/src/bucketer.spec.ts +165 -0
  54. package/src/bucketer.ts +84 -0
  55. package/src/child.spec.ts +267 -0
  56. package/src/child.ts +285 -0
  57. package/src/compareVersions.ts +93 -0
  58. package/src/conditions.spec.ts +563 -353
  59. package/src/conditions.ts +46 -63
  60. package/src/datafileReader.spec.ts +396 -84
  61. package/src/datafileReader.ts +280 -60
  62. package/src/emitter.spec.ts +27 -86
  63. package/src/emitter.ts +38 -32
  64. package/src/evaluate.ts +349 -258
  65. package/src/events.spec.ts +154 -0
  66. package/src/events.ts +83 -0
  67. package/src/helpers.ts +33 -0
  68. package/src/hooks.ts +88 -0
  69. package/src/index.ts +3 -2
  70. package/src/instance.spec.ts +305 -489
  71. package/src/instance.ts +247 -391
  72. package/src/logger.spec.ts +212 -134
  73. package/src/logger.ts +36 -36
  74. package/src/murmurhash.ts +71 -0
  75. package/coverage/lcov-report/bucket.ts.html +0 -502
  76. package/dist/bucket.d.ts +0 -30
  77. package/dist/feature.d.ts +0 -16
  78. package/dist/segments.d.ts +0 -5
  79. package/lib/bucket.d.ts +0 -30
  80. package/lib/feature.d.ts +0 -16
  81. package/lib/segments.d.ts +0 -5
  82. package/src/bucket.spec.ts +0 -37
  83. package/src/bucket.ts +0 -139
  84. package/src/feature.ts +0 -137
  85. package/src/segments.spec.ts +0 -468
  86. 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 "./bucket";
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";