@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
@@ -3,7 +3,7 @@
3
3
  <html lang="en">
4
4
 
5
5
  <head>
6
- <title>Code coverage report for segments.ts</title>
6
+ <title>Code coverage report for bucketer.ts</title>
7
7
  <meta charset="utf-8" />
8
8
  <link rel="stylesheet" href="prettify.css" />
9
9
  <link rel="stylesheet" href="base.css" />
@@ -19,34 +19,34 @@
19
19
  <body>
20
20
  <div class='wrapper'>
21
21
  <div class='pad1'>
22
- <h1><a href="index.html">All files</a> segments.ts</h1>
22
+ <h1><a href="index.html">All files</a> bucketer.ts</h1>
23
23
  <div class='clearfix'>
24
24
 
25
25
  <div class='fl pad1y space-right2'>
26
- <span class="strong">92% </span>
26
+ <span class="strong">94.11% </span>
27
27
  <span class="quiet">Statements</span>
28
- <span class='fraction'>23/25</span>
28
+ <span class='fraction'>32/34</span>
29
29
  </div>
30
30
 
31
31
 
32
32
  <div class='fl pad1y space-right2'>
33
- <span class="strong">100% </span>
33
+ <span class="strong">92.85% </span>
34
34
  <span class="quiet">Branches</span>
35
- <span class='fraction'>14/14</span>
35
+ <span class='fraction'>13/14</span>
36
36
  </div>
37
37
 
38
38
 
39
39
  <div class='fl pad1y space-right2'>
40
40
  <span class="strong">100% </span>
41
41
  <span class="quiet">Functions</span>
42
- <span class='fraction'>6/6</span>
42
+ <span class='fraction'>3/3</span>
43
43
  </div>
44
44
 
45
45
 
46
46
  <div class='fl pad1y space-right2'>
47
- <span class="strong">92% </span>
47
+ <span class="strong">94.11% </span>
48
48
  <span class="quiet">Lines</span>
49
- <span class='fraction'>23/25</span>
49
+ <span class='fraction'>32/34</span>
50
50
  </div>
51
51
 
52
52
 
@@ -121,121 +121,199 @@
121
121
  <a name='L56'></a><a href='#L56'>56</a>
122
122
  <a name='L57'></a><a href='#L57'>57</a>
123
123
  <a name='L58'></a><a href='#L58'>58</a>
124
- <a name='L59'></a><a href='#L59'>59</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
125
- <span class="cline-any cline-yes">2x</span>
124
+ <a name='L59'></a><a href='#L59'>59</a>
125
+ <a name='L60'></a><a href='#L60'>60</a>
126
+ <a name='L61'></a><a href='#L61'>61</a>
127
+ <a name='L62'></a><a href='#L62'>62</a>
128
+ <a name='L63'></a><a href='#L63'>63</a>
129
+ <a name='L64'></a><a href='#L64'>64</a>
130
+ <a name='L65'></a><a href='#L65'>65</a>
131
+ <a name='L66'></a><a href='#L66'>66</a>
132
+ <a name='L67'></a><a href='#L67'>67</a>
133
+ <a name='L68'></a><a href='#L68'>68</a>
134
+ <a name='L69'></a><a href='#L69'>69</a>
135
+ <a name='L70'></a><a href='#L70'>70</a>
136
+ <a name='L71'></a><a href='#L71'>71</a>
137
+ <a name='L72'></a><a href='#L72'>72</a>
138
+ <a name='L73'></a><a href='#L73'>73</a>
139
+ <a name='L74'></a><a href='#L74'>74</a>
140
+ <a name='L75'></a><a href='#L75'>75</a>
141
+ <a name='L76'></a><a href='#L76'>76</a>
142
+ <a name='L77'></a><a href='#L77'>77</a>
143
+ <a name='L78'></a><a href='#L78'>78</a>
144
+ <a name='L79'></a><a href='#L79'>79</a>
145
+ <a name='L80'></a><a href='#L80'>80</a>
146
+ <a name='L81'></a><a href='#L81'>81</a>
147
+ <a name='L82'></a><a href='#L82'>82</a>
148
+ <a name='L83'></a><a href='#L83'>83</a>
149
+ <a name='L84'></a><a href='#L84'>84</a>
150
+ <a name='L85'></a><a href='#L85'>85</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
126
151
  <span class="cline-any cline-neutral">&nbsp;</span>
127
152
  <span class="cline-any cline-neutral">&nbsp;</span>
153
+ <span class="cline-any cline-yes">3x</span>
154
+ <span class="cline-any cline-yes">3x</span>
128
155
  <span class="cline-any cline-neutral">&nbsp;</span>
129
- <span class="cline-any cline-yes">2x</span>
130
- <span class="cline-any cline-yes">133x</span>
131
156
  <span class="cline-any cline-neutral">&nbsp;</span>
132
157
  <span class="cline-any cline-neutral">&nbsp;</span>
133
- <span class="cline-any cline-yes">2x</span>
134
158
  <span class="cline-any cline-neutral">&nbsp;</span>
159
+ <span class="cline-any cline-yes">3x</span>
160
+ <span class="cline-any cline-yes">3x</span>
135
161
  <span class="cline-any cline-neutral">&nbsp;</span>
162
+ <span class="cline-any cline-yes">3x</span>
136
163
  <span class="cline-any cline-neutral">&nbsp;</span>
164
+ <span class="cline-any cline-yes">3x</span>
165
+ <span class="cline-any cline-yes">175x</span>
166
+ <span class="cline-any cline-yes">175x</span>
137
167
  <span class="cline-any cline-neutral">&nbsp;</span>
168
+ <span class="cline-any cline-yes">175x</span>
138
169
  <span class="cline-any cline-neutral">&nbsp;</span>
139
- <span class="cline-any cline-yes">342x</span>
140
- <span class="cline-any cline-yes">84x</span>
141
170
  <span class="cline-any cline-neutral">&nbsp;</span>
142
171
  <span class="cline-any cline-neutral">&nbsp;</span>
143
- <span class="cline-any cline-yes">258x</span>
144
- <span class="cline-any cline-yes">133x</span>
145
172
  <span class="cline-any cline-neutral">&nbsp;</span>
146
- <span class="cline-any cline-yes">133x</span>
147
- <span class="cline-any cline-yes">133x</span>
173
+ <span class="cline-any cline-neutral">&nbsp;</span>
174
+ <span class="cline-any cline-yes">3x</span>
175
+ <span class="cline-any cline-neutral">&nbsp;</span>
148
176
  <span class="cline-any cline-neutral">&nbsp;</span>
149
177
  <span class="cline-any cline-neutral">&nbsp;</span>
150
- <span class="cline-any cline-no">&nbsp;</span>
151
178
  <span class="cline-any cline-neutral">&nbsp;</span>
152
179
  <span class="cline-any cline-neutral">&nbsp;</span>
153
- <span class="cline-any cline-yes">125x</span>
154
- <span class="cline-any cline-yes">125x</span>
155
- <span class="cline-any cline-yes">15x</span>
156
- <span class="cline-any cline-yes">24x</span>
157
180
  <span class="cline-any cline-neutral">&nbsp;</span>
158
181
  <span class="cline-any cline-neutral">&nbsp;</span>
159
182
  <span class="cline-any cline-neutral">&nbsp;</span>
160
- <span class="cline-any cline-yes">110x</span>
161
- <span class="cline-any cline-yes">8x</span>
162
- <span class="cline-any cline-yes">12x</span>
163
183
  <span class="cline-any cline-neutral">&nbsp;</span>
184
+ <span class="cline-any cline-yes">3x</span>
164
185
  <span class="cline-any cline-neutral">&nbsp;</span>
165
186
  <span class="cline-any cline-neutral">&nbsp;</span>
166
- <span class="cline-any cline-yes">102x</span>
167
- <span class="cline-any cline-yes">10x</span>
168
187
  <span class="cline-any cline-neutral">&nbsp;</span>
169
- <span class="cline-any cline-yes">10x</span>
170
188
  <span class="cline-any cline-neutral">&nbsp;</span>
171
189
  <span class="cline-any cline-neutral">&nbsp;</span>
172
190
  <span class="cline-any cline-neutral">&nbsp;</span>
191
+ <span class="cline-any cline-yes">172x</span>
173
192
  <span class="cline-any cline-neutral">&nbsp;</span>
174
- <span class="cline-any cline-yes">92x</span>
175
- <span class="cline-any cline-yes">92x</span>
176
- <span class="cline-any cline-yes">103x</span>
177
193
  <span class="cline-any cline-neutral">&nbsp;</span>
178
194
  <span class="cline-any cline-neutral">&nbsp;</span>
179
195
  <span class="cline-any cline-neutral">&nbsp;</span>
196
+ <span class="cline-any cline-yes">172x</span>
197
+ <span class="cline-any cline-yes">160x</span>
198
+ <span class="cline-any cline-yes">160x</span>
199
+ <span class="cline-any cline-yes">12x</span>
200
+ <span class="cline-any cline-yes">5x</span>
201
+ <span class="cline-any cline-yes">5x</span>
202
+ <span class="cline-any cline-yes">7x</span>
203
+ <span class="cline-any cline-yes">7x</span>
204
+ <span class="cline-any cline-yes">7x</span>
205
+ <span class="cline-any cline-neutral">&nbsp;</span>
180
206
  <span class="cline-any cline-no">&nbsp;</span>
181
207
  <span class="cline-any cline-neutral">&nbsp;</span>
182
- <span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { Context, GroupSegment, Segment, Condition } from "@featurevisor/types";
183
- import { allConditionsAreMatched } from "./conditions";
184
- import { DatafileReader } from "./datafileReader";
208
+ <span class="cline-any cline-no">&nbsp;</span>
209
+ <span class="cline-any cline-neutral">&nbsp;</span>
210
+ <span class="cline-any cline-neutral">&nbsp;</span>
211
+ <span class="cline-any cline-yes">172x</span>
212
+ <span class="cline-any cline-neutral">&nbsp;</span>
213
+ <span class="cline-any cline-yes">172x</span>
214
+ <span class="cline-any cline-yes">184x</span>
215
+ <span class="cline-any cline-neutral">&nbsp;</span>
216
+ <span class="cline-any cline-yes">184x</span>
217
+ <span class="cline-any cline-yes">16x</span>
218
+ <span class="cline-any cline-neutral">&nbsp;</span>
219
+ <span class="cline-any cline-neutral">&nbsp;</span>
220
+ <span class="cline-any cline-yes">168x</span>
221
+ <span class="cline-any cline-yes">157x</span>
222
+ <span class="cline-any cline-neutral">&nbsp;</span>
223
+ <span class="cline-any cline-neutral">&nbsp;</span>
224
+ <span class="cline-any cline-yes">11x</span>
225
+ <span class="cline-any cline-yes">7x</span>
226
+ <span class="cline-any cline-neutral">&nbsp;</span>
227
+ <span class="cline-any cline-neutral">&nbsp;</span>
228
+ <span class="cline-any cline-neutral">&nbsp;</span>
229
+ <span class="cline-any cline-neutral">&nbsp;</span>
230
+ <span class="cline-any cline-yes">172x</span>
231
+ <span class="cline-any cline-neutral">&nbsp;</span>
232
+ <span class="cline-any cline-yes">172x</span>
233
+ <span class="cline-any cline-neutral">&nbsp;</span>
234
+ <span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import type { BucketKey, Context, AttributeValue, FeatureKey, BucketBy } from "@featurevisor/types";
235
+ &nbsp;
185
236
  import { Logger } from "./logger";
237
+ import { getValueFromContext } from "./conditions";
238
+ import { MurmurHashV3 } from "./murmurhash";
239
+ &nbsp;
240
+ /**
241
+ * Generic hashing
242
+ */
243
+ const HASH_SEED = 1;
244
+ const MAX_HASH_VALUE = Math.pow(2, 32);
186
245
  &nbsp;
187
- export function segmentIsMatched(segment: Segment, context: Context, logger: Logger): boolean {
188
- return allConditionsAreMatched(segment.conditions as Condition | Condition[], context, logger);
246
+ export const MAX_BUCKETED_NUMBER = 100000; // 100% * 1000 to include three decimal places in the same integer value
247
+ &nbsp;
248
+ export function getBucketedNumber(bucketKey: string): number {
249
+ const hashValue = MurmurHashV3(bucketKey, HASH_SEED);
250
+ const ratio = hashValue / MAX_HASH_VALUE;
251
+ &nbsp;
252
+ return Math.floor(ratio * MAX_BUCKETED_NUMBER);
189
253
  }
190
254
  &nbsp;
191
- export function allGroupSegmentsAreMatched(
192
- groupSegments: GroupSegment | GroupSegment[] | "*",
193
- context: Context,
194
- datafileReader: DatafileReader,
195
- logger: Logger,
196
- ): boolean {
197
- if (groupSegments === "*") {
198
- return true;
199
- }
255
+ /**
256
+ * Bucket key
257
+ */
258
+ const DEFAULT_BUCKET_KEY_SEPARATOR = ".";
200
259
  &nbsp;
201
- if (typeof groupSegments === "string") {
202
- const segment = datafileReader.getSegment(groupSegments);
260
+ export interface GetBucketKeyOptions {
261
+ featureKey: FeatureKey;
262
+ bucketBy: BucketBy;
263
+ context: Context;
203
264
  &nbsp;
204
- if (segment) {
205
- return segmentIsMatched(segment, context, logger);
206
- }
265
+ logger: Logger;
266
+ }
267
+ &nbsp;
268
+ export function getBucketKey(options: GetBucketKeyOptions): BucketKey {
269
+ const {
270
+ featureKey,
271
+ bucketBy,
272
+ context,
273
+ &nbsp;
274
+ logger,
275
+ } = options;
276
+ &nbsp;
277
+ let type;
278
+ let attributeKeys;
279
+ &nbsp;
280
+ if (typeof bucketBy === "string") {
281
+ type = "plain";
282
+ attributeKeys = [bucketBy];
283
+ } else if (Array.isArray(bucketBy)) {
284
+ type = "and";
285
+ attributeKeys = bucketBy;
286
+ } else if (typeof bucketBy === "object" &amp;&amp; Array.isArray(bucketBy.or)) {
287
+ type = "or";
288
+ attributeKeys = bucketBy.or;
289
+ } else <span class="missing-if-branch" title="else path not taken" >E</span>{
290
+ <span class="cstat-no" title="statement not covered" > logger.error("invalid bucketBy", { featureKey, bucketBy });</span>
207
291
  &nbsp;
208
- <span class="cstat-no" title="statement not covered" > return false;</span>
292
+ <span class="cstat-no" title="statement not covered" > throw new Error("invalid bucketBy");</span>
209
293
  }
210
294
  &nbsp;
211
- if (typeof groupSegments === "object") {
212
- if ("and" in groupSegments &amp;&amp; Array.isArray(groupSegments.and)) {
213
- return groupSegments.and.every((groupSegment) =&gt;
214
- allGroupSegmentsAreMatched(groupSegment, context, datafileReader, logger),
215
- );
216
- }
295
+ const bucketKey: AttributeValue[] = [];
296
+ &nbsp;
297
+ attributeKeys.forEach((attributeKey) =&gt; {
298
+ const attributeValue = getValueFromContext(context, attributeKey);
217
299
  &nbsp;
218
- if ("or" in groupSegments &amp;&amp; Array.isArray(groupSegments.or)) {
219
- return groupSegments.or.some((groupSegment) =&gt;
220
- allGroupSegmentsAreMatched(groupSegment, context, datafileReader, logger),
221
- );
300
+ if (typeof attributeValue === "undefined") {
301
+ return;
222
302
  }
223
303
  &nbsp;
224
- if ("not" in groupSegments &amp;&amp; Array.isArray(groupSegments.not)) {
225
- return groupSegments.not.every(
226
- (groupSegment) =&gt;
227
- allGroupSegmentsAreMatched(groupSegment, context, datafileReader, logger) === false,
228
- );
304
+ if (type === "plain" || type === "and") {
305
+ bucketKey.push(attributeValue);
306
+ } else {
307
+ // or
308
+ if (bucketKey.length === 0) {
309
+ bucketKey.push(attributeValue);
310
+ }
229
311
  }
230
- }
312
+ });
231
313
  &nbsp;
232
- if (Array.isArray(groupSegments)) {
233
- return groupSegments.every((groupSegment) =&gt;
234
- allGroupSegmentsAreMatched(groupSegment, context, datafileReader, logger),
235
- );
236
- }
314
+ bucketKey.push(featureKey);
237
315
  &nbsp;
238
- <span class="cstat-no" title="statement not covered" > return false;</span>
316
+ return bucketKey.join(DEFAULT_BUCKET_KEY_SEPARATOR);
239
317
  }
240
318
  &nbsp;</pre></td></tr></table></pre>
241
319
 
@@ -244,7 +322,7 @@ export function allGroupSegmentsAreMatched(
244
322
  <div class='footer quiet pad2 space-top1 center small'>
245
323
  Code coverage generated by
246
324
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
247
- at 2025-03-31T21:26:29.644Z
325
+ at 2025-07-13T22:31:55.309Z
248
326
  </div>
249
327
  <script src="prettify.js"></script>
250
328
  <script>