@featurevisor/core 0.15.0 → 0.17.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.
@@ -1,13 +1,12 @@
1
- import { DatafileContent, GroupSegment, ParsedFeature } from "@featurevisor/types";
2
- import { getNewTraffic } from "./traffic";
1
+ import { getTraffic } from "./traffic";
3
2
 
4
3
  describe("core: Traffic", function () {
5
4
  it("should be a function", function () {
6
- expect(typeof getNewTraffic).toEqual("function");
5
+ expect(typeof getTraffic).toEqual("function");
7
6
  });
8
7
 
9
8
  it("should allocate traffic for 100-0 weight on two variations", function () {
10
- const result = getNewTraffic(
9
+ const result = getTraffic(
11
10
  // parsed variations from YAML
12
11
  [
13
12
  {
@@ -42,10 +41,10 @@ describe("core: Traffic", function () {
42
41
  {
43
42
  variation: "on",
44
43
  percentage: 80000,
45
- },
46
- {
47
- variation: "off",
48
- percentage: 0,
44
+ range: {
45
+ start: 0,
46
+ end: 80000,
47
+ },
49
48
  },
50
49
  ],
51
50
  },
@@ -53,7 +52,7 @@ describe("core: Traffic", function () {
53
52
  });
54
53
 
55
54
  it("should allocate traffic for 50-50 weight on two variations", function () {
56
- const result = getNewTraffic(
55
+ const result = getTraffic(
57
56
  // parsed variations from YAML
58
57
  [
59
58
  {
@@ -88,10 +87,18 @@ describe("core: Traffic", function () {
88
87
  {
89
88
  variation: "on",
90
89
  percentage: 40000,
90
+ range: {
91
+ start: 0,
92
+ end: 40000,
93
+ },
91
94
  },
92
95
  {
93
96
  variation: "off",
94
97
  percentage: 40000,
98
+ range: {
99
+ start: 40000,
100
+ end: 80000,
101
+ },
95
102
  },
96
103
  ],
97
104
  },
@@ -99,7 +106,7 @@ describe("core: Traffic", function () {
99
106
  });
100
107
 
101
108
  it("should allocate traffic for weight with two decimal places among three variations", function () {
102
- const result = getNewTraffic(
109
+ const result = getTraffic(
103
110
  // parsed variations from YAML
104
111
  [
105
112
  {
@@ -138,14 +145,26 @@ describe("core: Traffic", function () {
138
145
  {
139
146
  variation: "yes",
140
147
  percentage: 33330,
148
+ range: {
149
+ start: 0,
150
+ end: 33330,
151
+ },
141
152
  },
142
153
  {
143
154
  variation: "no",
144
155
  percentage: 33330,
156
+ range: {
157
+ start: 33330,
158
+ end: 66660,
159
+ },
145
160
  },
146
161
  {
147
162
  variation: "maybe",
148
163
  percentage: 33340,
164
+ range: {
165
+ start: 66660,
166
+ end: 100000,
167
+ },
149
168
  },
150
169
  ],
151
170
  },
@@ -153,7 +172,7 @@ describe("core: Traffic", function () {
153
172
  });
154
173
 
155
174
  it("should allocate against previous known allocation, increasing from 80% to 90%, with same variations and weight", function () {
156
- const result = getNewTraffic(
175
+ const result = getTraffic(
157
176
  // parsed variations from YAML
158
177
  [
159
178
  {
@@ -195,10 +214,18 @@ describe("core: Traffic", function () {
195
214
  {
196
215
  variation: "on",
197
216
  percentage: 40000,
217
+ range: {
218
+ start: 0,
219
+ end: 40000,
220
+ },
198
221
  },
199
222
  {
200
223
  variation: "off",
201
224
  percentage: 40000,
225
+ range: {
226
+ start: 40000,
227
+ end: 80000,
228
+ },
202
229
  },
203
230
  ],
204
231
  },
@@ -216,20 +243,36 @@ describe("core: Traffic", function () {
216
243
  {
217
244
  variation: "on",
218
245
  percentage: 40000,
246
+ range: {
247
+ start: 0,
248
+ end: 40000,
249
+ },
219
250
  },
220
251
  {
221
252
  variation: "off",
222
253
  percentage: 40000,
254
+ range: {
255
+ start: 40000,
256
+ end: 80000,
257
+ },
223
258
  },
224
259
 
225
260
  // new
226
261
  {
227
262
  variation: "on",
228
263
  percentage: 5000,
264
+ range: {
265
+ start: 80000,
266
+ end: 85000,
267
+ },
229
268
  },
230
269
  {
231
270
  variation: "off",
232
271
  percentage: 5000,
272
+ range: {
273
+ start: 85000,
274
+ end: 90000,
275
+ },
233
276
  },
234
277
  ],
235
278
  },
@@ -237,7 +280,7 @@ describe("core: Traffic", function () {
237
280
  });
238
281
 
239
282
  it("should allocate against previous known allocation, decreasing from 80% to 70%, with same variations and weight", function () {
240
- const result = getNewTraffic(
283
+ const result = getTraffic(
241
284
  // parsed variations from YAML
242
285
  [
243
286
  {
@@ -279,10 +322,18 @@ describe("core: Traffic", function () {
279
322
  {
280
323
  variation: "on",
281
324
  percentage: 40000,
325
+ range: {
326
+ start: 0,
327
+ end: 40000,
328
+ },
282
329
  },
283
330
  {
284
331
  variation: "off",
285
332
  percentage: 40000,
333
+ range: {
334
+ start: 40000,
335
+ end: 80000,
336
+ },
286
337
  },
287
338
  ],
288
339
  },
@@ -299,10 +350,18 @@ describe("core: Traffic", function () {
299
350
  {
300
351
  variation: "on",
301
352
  percentage: 35000,
353
+ range: {
354
+ start: 0,
355
+ end: 35000,
356
+ },
302
357
  },
303
358
  {
304
359
  variation: "off",
305
360
  percentage: 35000,
361
+ range: {
362
+ start: 35000,
363
+ end: 70000,
364
+ },
306
365
  },
307
366
  ],
308
367
  },
@@ -310,7 +369,7 @@ describe("core: Traffic", function () {
310
369
  });
311
370
 
312
371
  it("should allocate against previous known allocation, increasing from 80% to 90%, with new added variation", function () {
313
- const result = getNewTraffic(
372
+ const result = getTraffic(
314
373
  // parsed variations from YAML
315
374
  [
316
375
  {
@@ -356,10 +415,18 @@ describe("core: Traffic", function () {
356
415
  {
357
416
  variation: "a",
358
417
  percentage: 40000,
418
+ range: {
419
+ start: 0,
420
+ end: 40000,
421
+ },
359
422
  },
360
423
  {
361
424
  variation: "b",
362
425
  percentage: 40000,
426
+ range: {
427
+ start: 40000,
428
+ end: 80000,
429
+ },
363
430
  },
364
431
  ],
365
432
  },
@@ -376,14 +443,26 @@ describe("core: Traffic", function () {
376
443
  {
377
444
  variation: "a",
378
445
  percentage: 29997,
446
+ range: {
447
+ start: 0,
448
+ end: 29997,
449
+ },
379
450
  },
380
451
  {
381
452
  variation: "b",
382
453
  percentage: 29997,
454
+ range: {
455
+ start: 29997,
456
+ end: 59994,
457
+ },
383
458
  },
384
459
  {
385
460
  variation: "c",
386
461
  percentage: 30006,
462
+ range: {
463
+ start: 59994,
464
+ end: 90000,
465
+ },
387
466
  },
388
467
  ],
389
468
  },
@@ -391,7 +470,7 @@ describe("core: Traffic", function () {
391
470
  });
392
471
 
393
472
  it("should allocate against previous known allocation, increasing from 80% to 100%, with new added variation, totalling 4 variations", function () {
394
- const result = getNewTraffic(
473
+ const result = getTraffic(
395
474
  // parsed variations from YAML
396
475
  [
397
476
  {
@@ -441,10 +520,18 @@ describe("core: Traffic", function () {
441
520
  {
442
521
  variation: "a",
443
522
  percentage: 40000,
523
+ range: {
524
+ start: 0,
525
+ end: 40000,
526
+ },
444
527
  },
445
528
  {
446
529
  variation: "b",
447
530
  percentage: 40000,
531
+ range: {
532
+ start: 40000,
533
+ end: 80000,
534
+ },
448
535
  },
449
536
  ],
450
537
  },
@@ -461,18 +548,34 @@ describe("core: Traffic", function () {
461
548
  {
462
549
  variation: "a",
463
550
  percentage: 25000,
551
+ range: {
552
+ start: 0,
553
+ end: 25000,
554
+ },
464
555
  },
465
556
  {
466
557
  variation: "b",
467
558
  percentage: 25000,
559
+ range: {
560
+ start: 25000,
561
+ end: 50000,
562
+ },
468
563
  },
469
564
  {
470
565
  variation: "c",
471
566
  percentage: 25000,
567
+ range: {
568
+ start: 50000,
569
+ end: 75000,
570
+ },
472
571
  },
473
572
  {
474
573
  variation: "d",
475
574
  percentage: 25000,
575
+ range: {
576
+ start: 75000,
577
+ end: 100000,
578
+ },
476
579
  },
477
580
  ],
478
581
  },
@@ -480,7 +583,7 @@ describe("core: Traffic", function () {
480
583
  });
481
584
 
482
585
  it("should allocate against previous known allocation, staying at same 100%, removing variations from 4 to 2", function () {
483
- const result = getNewTraffic(
586
+ const result = getTraffic(
484
587
  // parsed variations from YAML
485
588
  [
486
589
  {
@@ -530,18 +633,34 @@ describe("core: Traffic", function () {
530
633
  {
531
634
  variation: "a",
532
635
  percentage: 25000,
636
+ range: {
637
+ start: 0,
638
+ end: 25000,
639
+ },
533
640
  },
534
641
  {
535
642
  variation: "b",
536
643
  percentage: 25000,
644
+ range: {
645
+ start: 25000,
646
+ end: 50000,
647
+ },
537
648
  },
538
649
  {
539
650
  variation: "c",
540
651
  percentage: 25000,
652
+ range: {
653
+ start: 50000,
654
+ end: 75000,
655
+ },
541
656
  },
542
657
  {
543
658
  variation: "d",
544
659
  percentage: 25000,
660
+ range: {
661
+ start: 75000,
662
+ end: 100000,
663
+ },
545
664
  },
546
665
  ],
547
666
  },
@@ -558,10 +677,18 @@ describe("core: Traffic", function () {
558
677
  {
559
678
  variation: "a",
560
679
  percentage: 50000,
680
+ range: {
681
+ start: 0,
682
+ end: 50000,
683
+ },
561
684
  },
562
685
  {
563
686
  variation: "b",
564
687
  percentage: 50000,
688
+ range: {
689
+ start: 50000,
690
+ end: 100000,
691
+ },
565
692
  },
566
693
  ],
567
694
  },
@@ -569,7 +696,7 @@ describe("core: Traffic", function () {
569
696
  });
570
697
 
571
698
  it("should allocate against previous known allocation, decreasing from 100% to 80%, removing variations from 4 to 2", function () {
572
- const result = getNewTraffic(
699
+ const result = getTraffic(
573
700
  // parsed variations from YAML
574
701
  [
575
702
  {
@@ -619,18 +746,34 @@ describe("core: Traffic", function () {
619
746
  {
620
747
  variation: "a",
621
748
  percentage: 25000,
749
+ range: {
750
+ start: 0,
751
+ end: 25000,
752
+ },
622
753
  },
623
754
  {
624
755
  variation: "b",
625
756
  percentage: 25000,
757
+ range: {
758
+ start: 25000,
759
+ end: 50000,
760
+ },
626
761
  },
627
762
  {
628
763
  variation: "c",
629
764
  percentage: 25000,
765
+ range: {
766
+ start: 50000,
767
+ end: 75000,
768
+ },
630
769
  },
631
770
  {
632
771
  variation: "d",
633
772
  percentage: 25000,
773
+ range: {
774
+ start: 75000,
775
+ end: 100000,
776
+ },
634
777
  },
635
778
  ],
636
779
  },
@@ -647,10 +790,18 @@ describe("core: Traffic", function () {
647
790
  {
648
791
  variation: "a",
649
792
  percentage: 40000,
793
+ range: {
794
+ start: 0,
795
+ end: 40000,
796
+ },
650
797
  },
651
798
  {
652
799
  variation: "b",
653
800
  percentage: 40000,
801
+ range: {
802
+ start: 40000,
803
+ end: 80000,
804
+ },
654
805
  },
655
806
  ],
656
807
  },
package/src/traffic.ts CHANGED
@@ -1,140 +1,154 @@
1
- import { Rule, ExistingFeature, Traffic, Variation } from "@featurevisor/types";
1
+ import { Rule, ExistingFeature, Traffic, Variation, Range, Percentage } from "@featurevisor/types";
2
2
  import { MAX_BUCKETED_NUMBER } from "@featurevisor/sdk";
3
3
 
4
- export function getNewTraffic(
4
+ import { getAllocation, getUpdatedAvailableRangesAfterFilling } from "./allocator";
5
+
6
+ export function detectIfVariationsChanged(
7
+ yamlVariations: Variation[], // as exists in latest YAML
8
+ existingFeature?: ExistingFeature, // from state file
9
+ ): boolean {
10
+ if (!existingFeature) {
11
+ return false;
12
+ }
13
+
14
+ return (
15
+ JSON.stringify(
16
+ existingFeature.variations.map(({ value, weight }) => ({
17
+ value,
18
+ weight,
19
+ })),
20
+ ) !== JSON.stringify(yamlVariations.map(({ value, weight }) => ({ value, weight })))
21
+ );
22
+ }
23
+
24
+ export function getRulePercentageDiff(
25
+ trafficPercentage: Percentage, // 0 to 100k
26
+ existingTrafficRule,
27
+ ): number {
28
+ if (!existingTrafficRule) {
29
+ return 0;
30
+ }
31
+
32
+ const existingPercentage = existingTrafficRule.percentage;
33
+
34
+ return trafficPercentage - existingPercentage;
35
+ }
36
+
37
+ export function detectIfRangesChanged(
38
+ availableRanges: Range[], // as exists in latest YAML
39
+ existingFeature?: ExistingFeature, // from state file
40
+ ): boolean {
41
+ if (!existingFeature) {
42
+ return false;
43
+ }
44
+
45
+ if (!existingFeature.ranges) {
46
+ return false;
47
+ }
48
+
49
+ return JSON.stringify(existingFeature.ranges) !== JSON.stringify(availableRanges);
50
+ }
51
+
52
+ export function getTraffic(
5
53
  // from current YAML
6
54
  variations: Variation[],
7
55
  parsedRules: Rule[],
8
-
9
56
  // from previous release
10
57
  existingFeature: ExistingFeature | undefined,
58
+ // ranges from group slots
59
+ ranges?: Range[],
11
60
  ): Traffic[] {
12
61
  const result: Traffic[] = [];
13
62
 
14
- parsedRules.forEach((parsedRollout) => {
15
- const rolloutPercentage = parsedRollout.percentage;
63
+ // @TODO: may be pass from builder directly?
64
+ const availableRanges =
65
+ ranges && ranges.length > 0 ? ranges : [{ start: 0, end: MAX_BUCKETED_NUMBER }];
66
+
67
+ parsedRules.forEach(function (parsedRule) {
68
+ const rulePercentage = parsedRule.percentage; // 0 - 100
16
69
 
17
70
  const traffic: Traffic = {
18
- key: parsedRollout.key, // @TODO: not needed in datafile. keep it for now
71
+ key: parsedRule.key, // @TODO: not needed in datafile. keep it for now
19
72
  segments:
20
- typeof parsedRollout.segments !== "string"
21
- ? JSON.stringify(parsedRollout.segments)
22
- : parsedRollout.segments,
23
- percentage: rolloutPercentage * (MAX_BUCKETED_NUMBER / 100),
73
+ typeof parsedRule.segments !== "string"
74
+ ? JSON.stringify(parsedRule.segments)
75
+ : parsedRule.segments,
76
+ percentage: rulePercentage * (MAX_BUCKETED_NUMBER / 100),
24
77
  allocation: [],
25
78
  };
26
79
 
27
- if (parsedRollout.variables) {
28
- traffic.variables = parsedRollout.variables;
80
+ // overrides
81
+ if (parsedRule.variables) {
82
+ traffic.variables = parsedRule.variables;
29
83
  }
30
84
 
31
- if (parsedRollout.variation) {
32
- traffic.variation = parsedRollout.variation;
85
+ if (parsedRule.variation) {
86
+ traffic.variation = parsedRule.variation;
33
87
  }
34
88
 
35
- const existingTrafficRollout = existingFeature?.traffic.find(
36
- (t) => t.key === parsedRollout.key,
37
- );
38
-
39
- // @TODO: handle if Variations changed (added/removed, or weight changed)
40
-
41
- // - new variation added
42
- // - variation removed
43
- // - variation weight changed
44
- //
45
- // make it better by maintaining as much of the previous bucketing as possible
46
- const variationsChanged = existingFeature
47
- ? JSON.stringify(
48
- existingFeature.variations.map(({ value, weight }) => ({
49
- value,
50
- weight,
51
- })),
52
- ) !== JSON.stringify(variations.map(({ value, weight }) => ({ value, weight })))
53
- : false;
54
-
55
- let diffPercentage = 0;
56
-
57
- if (existingTrafficRollout) {
58
- diffPercentage =
59
- rolloutPercentage - existingTrafficRollout.percentage / (MAX_BUCKETED_NUMBER / 100);
60
-
61
- if (
62
- diffPercentage > 0 &&
63
- !variationsChanged // if variations changed, we need to re-bucket
64
- ) {
65
- // increase: build on top of existing allocations
66
-
67
- traffic.allocation = existingTrafficRollout.allocation.map(({ variation, percentage }) => {
68
- return {
69
- variation,
70
- percentage,
71
- };
72
- });
73
- }
89
+ // detect changes
90
+ const variationsChanged = detectIfVariationsChanged(variations, existingFeature);
91
+ const existingTrafficRule = existingFeature?.traffic.find((t) => t.key === parsedRule.key);
92
+ const rulePercentageDiff = getRulePercentageDiff(traffic.percentage, existingTrafficRule);
93
+ const rangesChanged = detectIfRangesChanged(availableRanges, existingFeature);
94
+
95
+ const needsRebucketing =
96
+ !existingTrafficRule || // new rule
97
+ variationsChanged || // variations changed
98
+ rulePercentageDiff <= 0 || // percentage decreased
99
+ rangesChanged; // belongs to a group, and group ranges changed
100
+
101
+ let updatedAvailableRanges = JSON.parse(JSON.stringify(availableRanges));
102
+
103
+ let lastEnd = 0;
104
+ if (existingTrafficRule && !needsRebucketing) {
105
+ // increase: build on top of existing allocations
106
+ let existingSum = 0;
107
+
108
+ traffic.allocation = existingTrafficRule.allocation.map(function ({
109
+ variation,
110
+ percentage, // @TODO: remove it in next breaking semver
111
+ range,
112
+ }) {
113
+ const result = {
114
+ variation,
115
+ percentage, // @TODO remove it in next breaking semver
116
+ range: range || {
117
+ start: lastEnd,
118
+ end: percentage,
119
+ },
120
+ };
121
+
122
+ existingSum += percentage || 0;
123
+ lastEnd = lastEnd + (percentage || 0);
124
+
125
+ return result;
126
+ });
127
+
128
+ updatedAvailableRanges = getUpdatedAvailableRangesAfterFilling(availableRanges, existingSum);
74
129
  }
75
130
 
76
- variations.forEach((variation) => {
77
- const newPercentage = parseInt(
78
- (
79
- ((variation.weight as number) / 100) *
80
- rolloutPercentage *
81
- (MAX_BUCKETED_NUMBER / 100)
82
- ).toFixed(2),
83
- );
131
+ variations.forEach(function (variation) {
132
+ const weight = variation.weight as number;
133
+ const percentage = weight * (MAX_BUCKETED_NUMBER / 100);
84
134
 
85
- if (!existingTrafficRollout || variationsChanged === true) {
86
- traffic.allocation.push({
87
- variation: variation.value,
88
- percentage: newPercentage,
89
- });
135
+ let toFillValue = needsRebucketing
136
+ ? percentage * (rulePercentage / 100) // whole value
137
+ : (weight / 100) * rulePercentageDiff; // incrementing
138
+ const rangesToFill = getAllocation(updatedAvailableRanges, toFillValue);
90
139
 
91
- return;
92
- }
93
-
94
- // const prevTotalWeightForVariation = existingTrafficRollout.allocation
95
- // .filter((a) => a.variation === variation.value)
96
- // .reduce((acc, curr) => acc + curr.percentage, 0);
97
-
98
- // const diffWeightForVariation = (variation.weight as number) - prevTotalWeightForVariation / (MAX_BUCKETED_NUMBER / 100));
99
-
100
- if (diffPercentage === 0) {
101
- // no change
102
- traffic.allocation.push({
103
- variation: variation.value,
104
- percentage: newPercentage,
105
- });
106
-
107
- return;
108
- }
109
-
110
- if (diffPercentage > 0) {
111
- // increase - need to consistently bucket
140
+ rangesToFill.forEach(function (range) {
112
141
  traffic.allocation.push({
113
142
  variation: variation.value,
114
- percentage: parseInt(
115
- (
116
- (variation.weight as number) *
117
- (diffPercentage / 100) *
118
- (MAX_BUCKETED_NUMBER / 100)
119
- ).toFixed(2),
120
- ),
143
+ percentage: toFillValue, // @TODO remove it in next breaking semver
144
+ range,
121
145
  });
146
+ });
122
147
 
123
- return;
124
- }
125
-
126
- if (diffPercentage < 0) {
127
- // decrease - need to re-bucket
128
-
129
- // @TODO: can we maintain as much pre bucketed values as possible? to be close to consistent bucketing?
130
-
131
- traffic.allocation.push({
132
- variation: variation.value,
133
- percentage: newPercentage,
134
- });
135
-
136
- return;
137
- }
148
+ updatedAvailableRanges = getUpdatedAvailableRangesAfterFilling(
149
+ updatedAvailableRanges,
150
+ toFillValue,
151
+ );
138
152
  });
139
153
 
140
154
  result.push(traffic);