Skip to content

Commit

Permalink
feat: Change fractional custom op from percentage-based to relative w…
Browse files Browse the repository at this point in the history
…eighting. open-feature#946

Signed-off-by: Simon Schrottner <simon.schrottner@dynatrace.com>
  • Loading branch information
aepfli committed Jun 19, 2024
1 parent 0a4a8df commit 2d086ce
Show file tree
Hide file tree
Showing 9 changed files with 86 additions and 25 deletions.
35 changes: 34 additions & 1 deletion libs/providers/flagd-web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion libs/providers/flagd-web/schemas
2 changes: 1 addition & 1 deletion libs/providers/flagd/flagd-testbed
2 changes: 1 addition & 1 deletion libs/providers/flagd/schemas
41 changes: 25 additions & 16 deletions libs/shared/flagd-core/src/lib/targeting/fractional.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ export function fractionalFactory(logger: Logger) {
const bucket = (Math.abs(hash) / 2147483648) * 100;

let sum = 0;
for (let i = 0; i < bucketingList.length; i++) {
const bucketEntry = bucketingList[i];
for (let i = 0; i < bucketingList.fractions.length; i++) {
const bucketEntry = bucketingList.fractions[i];

sum += bucketEntry.fraction;
sum += relativeWeight(bucketingList.totalWeight, bucketEntry.fraction);

if (sum >= bucket) {
return bucketEntry.variant;
Expand All @@ -66,36 +66,45 @@ export function fractionalFactory(logger: Logger) {
};
}

function toBucketingList(from: unknown[]): { variant: string; fraction: number }[] {
function relativeWeight(totalWeight: number, weight: number): number {
if (weight == 0) {
return 0;
}
return (weight * 100) / totalWeight;
}
function toBucketingList(from: unknown[]): {
fractions: { variant: string; fraction: number }[];
totalWeight: number;
} {
// extract bucketing options
const bucketingArray: { variant: string; fraction: number }[] = [];

let bucketSum = 0;
let totalWeight = 0;
for (let i = 0; i < from.length; i++) {
const entry = from[i];
if (!Array.isArray(entry)) {
throw new Error('Invalid bucket entries');
}

if (entry.length != 2) {
throw new Error('Invalid bucketing entry. Require two values - variant and percentage');
if (entry.length == 0) {
throw new Error('Invalid bucketing entry. Requires at least a variant');
}

if (typeof entry[0] !== 'string') {
throw new Error('Bucketing require variant to be present in string format');
}

if (typeof entry[1] !== 'number') {
throw new Error('Bucketing require bucketing percentage to be present');
let weight = 1;
if (entry.length >= 2) {
if (typeof entry[1] !== 'number') {
throw new Error('Bucketing require bucketing percentage to be present');
}
weight = entry[1];
}

bucketingArray.push({ fraction: entry[1], variant: entry[0] });
bucketSum += entry[1];
}

if (bucketSum != 100) {
throw new Error('Bucketing sum must add up to 100');
bucketingArray.push({ fraction: weight, variant: entry[0] });
totalWeight += weight;
}

return bucketingArray;
return { fractions: bucketingArray, totalWeight };
}
23 changes: 21 additions & 2 deletions libs/shared/flagd-core/src/lib/targeting/targeting.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,14 @@ describe('fractional operator', () => {

expect(targeting.applyTargeting('flagA', input, { targetingKey: 'bucketKeyB' })).toBe('blue');
});

it('should evaluate valid rule with targeting key although one does not have a fraction', () => {
const input = {
fractional: [['red', 1], ['blue']],
};

expect(targeting.applyTargeting('flagA', input, { targetingKey: 'bucketKeyB' })).toBe('blue');
});
});

describe('fractional operator should validate', () => {
Expand All @@ -188,15 +196,26 @@ describe('fractional operator should validate', () => {
targeting = new Targeting(logger);
});

it('bucket sum to be 100', () => {
it('bucket sum with sum bigger than 100', () => {
const input = {
fractional: [
['red', 55],
['blue', 55],
],
};

expect(targeting.applyTargeting('flagA', input, { targetingKey: 'key' })).toBe(null);
expect(targeting.applyTargeting('flagA', input, { targetingKey: 'key' })).toBe('blue');
});

it('bucket sum with sum lower than 100', () => {
const input = {
fractional: [
['red', 45],
['blue', 45],
],
};

expect(targeting.applyTargeting('flagA', input, { targetingKey: 'key' })).toBe('blue');
});

it('buckets properties to have variant and fraction', () => {
Expand Down

0 comments on commit 2d086ce

Please sign in to comment.