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