@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
package/src/bucket.ts DELETED
@@ -1,139 +0,0 @@
1
- import * as murmurhash from "murmurhash";
2
- import { Feature, BucketKey, BucketValue, Context, AttributeValue } from "@featurevisor/types";
3
-
4
- import { Logger } from "./logger";
5
-
6
- /**
7
- * Generic hashing
8
- */
9
- const HASH_SEED = 1;
10
- const MAX_HASH_VALUE = Math.pow(2, 32);
11
-
12
- export const MAX_BUCKETED_NUMBER = 100000; // 100% * 1000 to include three decimal places in the same integer value
13
-
14
- export function getBucketedNumber(bucketKey: string): number {
15
- const hashValue = murmurhash.v3(bucketKey, HASH_SEED);
16
- const ratio = hashValue / MAX_HASH_VALUE;
17
-
18
- return Math.floor(ratio * MAX_BUCKETED_NUMBER);
19
- }
20
-
21
- /**
22
- * Feature specific bucketing
23
- */
24
- export type ConfigureBucketKey = (
25
- feature: Feature,
26
- context: Context,
27
- bucketKey: BucketKey,
28
- ) => BucketKey;
29
-
30
- export interface BucketKeyOptions {
31
- feature: Feature;
32
- context: Context;
33
- logger: Logger;
34
- bucketKeySeparator?: string;
35
- configureBucketKey?: ConfigureBucketKey;
36
- }
37
-
38
- export function getBucketKey(options: BucketKeyOptions): BucketKey {
39
- const { feature, context, logger, bucketKeySeparator = ".", configureBucketKey } = options;
40
-
41
- const featureKey = feature.key;
42
-
43
- let type;
44
- let attributeKeys;
45
-
46
- if (typeof feature.bucketBy === "string") {
47
- type = "plain";
48
- attributeKeys = [feature.bucketBy];
49
- } else if (Array.isArray(feature.bucketBy)) {
50
- type = "and";
51
- attributeKeys = feature.bucketBy;
52
- } else if (typeof feature.bucketBy === "object" && Array.isArray(feature.bucketBy.or)) {
53
- type = "or";
54
- attributeKeys = feature.bucketBy.or;
55
- } else {
56
- logger.error("invalid bucketBy", { featureKey, bucketBy: feature.bucketBy });
57
-
58
- throw new Error("invalid bucketBy");
59
- }
60
-
61
- const bucketKey: AttributeValue[] = [];
62
-
63
- attributeKeys.forEach((attributeKey) => {
64
- const attributeValue = context[attributeKey];
65
-
66
- if (typeof attributeValue === "undefined") {
67
- return;
68
- }
69
-
70
- if (type === "plain" || type === "and") {
71
- bucketKey.push(attributeValue);
72
- } else {
73
- // or
74
- if (bucketKey.length === 0) {
75
- bucketKey.push(attributeValue);
76
- }
77
- }
78
- });
79
-
80
- bucketKey.push(featureKey);
81
-
82
- const result = bucketKey.join(bucketKeySeparator);
83
-
84
- if (configureBucketKey) {
85
- return configureBucketKey(feature, context, result);
86
- }
87
-
88
- return result;
89
- }
90
-
91
- export interface Bucket {
92
- bucketKey: BucketKey;
93
- bucketValue: BucketValue;
94
- }
95
-
96
- export type ConfigureBucketValue = (
97
- feature: Feature,
98
- context: Context,
99
- bucketValue: BucketValue,
100
- ) => BucketValue;
101
-
102
- export interface BucketValueOptions {
103
- // common with BucketKeyOptions
104
- feature: Feature;
105
- context: Context;
106
- logger: Logger;
107
- bucketKeySeparator?: string;
108
- configureBucketKey?: ConfigureBucketKey;
109
-
110
- // specific to BucketValueOptions
111
- configureBucketValue?: ConfigureBucketValue;
112
- }
113
-
114
- export function getBucket(options: BucketValueOptions): Bucket {
115
- const { feature, context, logger, bucketKeySeparator, configureBucketKey, configureBucketValue } =
116
- options;
117
- const bucketKey = getBucketKey({
118
- feature,
119
- context,
120
- logger,
121
- bucketKeySeparator,
122
- configureBucketKey,
123
- });
124
- const value = getBucketedNumber(bucketKey);
125
-
126
- if (configureBucketValue) {
127
- const configuredValue = configureBucketValue(feature, context, value);
128
-
129
- return {
130
- bucketKey,
131
- bucketValue: configuredValue,
132
- };
133
- }
134
-
135
- return {
136
- bucketKey,
137
- bucketValue: value,
138
- };
139
- }
package/src/feature.ts DELETED
@@ -1,141 +0,0 @@
1
- import { Allocation, Context, Traffic, Feature, Force } from "@featurevisor/types";
2
- import { DatafileReader } from "./datafileReader";
3
- import { allGroupSegmentsAreMatched } from "./segments";
4
- import { allConditionsAreMatched } from "./conditions";
5
- import { Logger } from "./logger";
6
-
7
- export function getMatchedAllocation(
8
- traffic: Traffic,
9
- bucketValue: number,
10
- ): Allocation | undefined {
11
- if (!traffic.allocation) {
12
- return undefined;
13
- }
14
-
15
- for (const allocation of traffic.allocation) {
16
- const [start, end] = allocation.range;
17
-
18
- if (allocation.range && start <= bucketValue && end >= bucketValue) {
19
- return allocation;
20
- }
21
- }
22
-
23
- return undefined;
24
- }
25
-
26
- export function parseFromStringifiedSegments(value) {
27
- if (typeof value === "string" && (value.startsWith("{") || value.startsWith("["))) {
28
- return JSON.parse(value);
29
- }
30
-
31
- return value;
32
- }
33
-
34
- export function getMatchedTraffic(
35
- traffic: Traffic[],
36
- context: Context,
37
- datafileReader: DatafileReader,
38
- logger: Logger,
39
- ): Traffic | undefined {
40
- const matchedTraffic = traffic.find((t) => {
41
- if (
42
- !allGroupSegmentsAreMatched(
43
- parseFromStringifiedSegments(t.segments),
44
- context,
45
- datafileReader,
46
- logger,
47
- )
48
- ) {
49
- return false;
50
- }
51
-
52
- return true;
53
- });
54
-
55
- if (matchedTraffic && matchedTraffic.percentage > 0) {
56
- return matchedTraffic;
57
- }
58
-
59
- return;
60
- }
61
-
62
- export interface MatchedTrafficAndAllocation {
63
- matchedTraffic: Traffic | undefined;
64
- matchedAllocation: Allocation | undefined;
65
- }
66
-
67
- export function getMatchedTrafficAndAllocation(
68
- traffic: Traffic[],
69
- context: Context,
70
- bucketValue: number,
71
- datafileReader: DatafileReader,
72
- logger: Logger,
73
- ): MatchedTrafficAndAllocation {
74
- const matchedTraffic = traffic.find((t) => {
75
- return allGroupSegmentsAreMatched(
76
- parseFromStringifiedSegments(t.segments),
77
- context,
78
- datafileReader,
79
- logger,
80
- );
81
- });
82
-
83
- if (!matchedTraffic) {
84
- return {
85
- matchedTraffic: undefined,
86
- matchedAllocation: undefined,
87
- };
88
- }
89
-
90
- const matchedAllocation = getMatchedAllocation(matchedTraffic, bucketValue);
91
-
92
- return {
93
- matchedTraffic,
94
- matchedAllocation,
95
- };
96
- }
97
-
98
- export interface ForceResult {
99
- force?: Force;
100
- forceIndex?: number;
101
- }
102
-
103
- export function findForceFromFeature(
104
- feature: Feature,
105
- context: Context,
106
- datafileReader: DatafileReader,
107
- logger: Logger,
108
- ): ForceResult {
109
- const result: ForceResult = {
110
- force: undefined,
111
- forceIndex: undefined,
112
- };
113
-
114
- if (!feature.force) {
115
- return result;
116
- }
117
-
118
- for (let i = 0; i < feature.force.length; i++) {
119
- const currentForce = feature.force[i];
120
-
121
- if (
122
- currentForce.conditions &&
123
- allConditionsAreMatched(currentForce.conditions, context, logger)
124
- ) {
125
- result.force = currentForce;
126
- result.forceIndex = i;
127
- break;
128
- }
129
-
130
- if (
131
- currentForce.segments &&
132
- allGroupSegmentsAreMatched(currentForce.segments, context, datafileReader, logger)
133
- ) {
134
- result.force = currentForce;
135
- result.forceIndex = i;
136
- break;
137
- }
138
- }
139
-
140
- return result;
141
- }