Skip to content

Commit

Permalink
feat!: allow overrides for fractional seed (#870)
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
Co-authored-by: Todd Baert <todd.baert@dynatrace.com>
  • Loading branch information
beeme1mr and toddbaert committed Apr 15, 2024
1 parent 0cddd68 commit 6c376b2
Show file tree
Hide file tree
Showing 9 changed files with 69 additions and 39 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,19 @@ jobs:

services:
flagd:
image: ghcr.io/open-feature/flagd-testbed:v0.5.2
image: ghcr.io/open-feature/flagd-testbed:v0.5.4
ports:
- 8013:8013
flagd-unstable:
image: ghcr.io/open-feature/flagd-testbed-unstable:v0.5.2
image: ghcr.io/open-feature/flagd-testbed-unstable:v0.5.4
ports:
- 8014:8013
sync:
image: ghcr.io/open-feature/sync-testbed:v0.5.2
image: ghcr.io/open-feature/sync-testbed:v0.5.4
ports:
- 9090:9090
sync-unstable:
image: ghcr.io/open-feature/sync-testbed-unstable:v0.5.2
image: ghcr.io/open-feature/sync-testbed-unstable:v0.5.4
ports:
- 9091:9090

Expand Down
25 changes: 25 additions & 0 deletions libs/providers/flagd/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
services:
flagd:
build:
context: test-harness
dockerfile: flagd/Dockerfile
ports:
- 8013:8013
flagd-unstable:
build:
context: test-harness
dockerfile: flagd/Dockerfile.unstable
ports:
- 8014:8013
flagd-sync:
build:
context: test-harness
dockerfile: sync/Dockerfile
ports:
- 9090:9090
flagd-sync-unstable:
build:
context: test-harness
dockerfile: sync/Dockerfile.unstable
ports:
- 9091:9090
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,32 @@ const evaluateStringFlagWithContext: StepsDefinitionCallbackFunction = ({ given,
});
};

const evaluateStringFlagWithFractional: StepsDefinitionCallbackFunction = ({ given, when, and, then }) => {
let flagKey: string;
let defaultValue: string;
const evaluationContext: EvaluationContext = {};

aFlagProviderIsSet(given);
when(/^a string flag with key "(.*)" is evaluated with default value "(.*)"$/, (key, defaultVal) => {
flagKey = key;
defaultValue = defaultVal;
});
and(
/^a context containing a nested property with outer key "(.*)" and inner key "(.*)", with value (.*)$/,
(outerKey: string, innerKey: string, value: string) => {
// we have to support string and non-string params in this test (we test invalid context value 3)
const valueNoQuotes = value.replaceAll('"', '');
evaluationContext[outerKey] = {
[innerKey]: parseInt(valueNoQuotes) || valueNoQuotes,
};
},
);
then(/^the returned value should be "(.*)"$/, async (expectedValue: string) => {
const value = await client.getStringValue(flagKey, defaultValue, evaluationContext);
expect(value).toEqual(expectedValue);
});
};

defineFeature(feature, (test) => {
beforeAll((done) => {
client.addHandler(ProviderEvents.Ready, async () => {
Expand All @@ -46,31 +72,11 @@ defineFeature(feature, (test) => {

test('Evaluator reuse', evaluateStringFlagWithContext);

test('Fractional operator', ({ given, when, and, then }) => {
let flagKey: string;
let defaultValue: string;
const evaluationContext: EvaluationContext = {};
test('Fractional operator', evaluateStringFlagWithFractional);

aFlagProviderIsSet(given);
when(/^a string flag with key "(.*)" is evaluated with default value "(.*)"$/, (key, defaultVal) => {
flagKey = key;
defaultValue = defaultVal;
});
and(
/^a context containing a nested property with outer key "(.*)" and inner key "(.*)", with value (.*)$/,
(outerKey: string, innerKey: string, value: string) => {
// we have to support string and non-string params in this test (we test invalid context value 3)
const valueNoQuotes = value.replaceAll('"', '');
evaluationContext[outerKey] = {
[innerKey]: parseInt(valueNoQuotes) || valueNoQuotes,
};
},
);
then(/^the returned value should be "(.*)"$/, async (expectedValue: string) => {
const value = await client.getStringValue(flagKey, defaultValue, evaluationContext);
expect(value).toEqual(expectedValue);
});
});
test('Fractional operator with shared seed', evaluateStringFlagWithFractional);

test('Second fractional operator with shared seed', evaluateStringFlagWithFractional);

test('Substring operators', evaluateStringFlagWithContext);

Expand Down
4 changes: 2 additions & 2 deletions libs/shared/flagd-core/src/lib/flagd-core.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ describe('flagd-core common flag definitions', () => {

it('should support fractional logic', () => {
const core = new FlagdCore();
const flagCfg = `{"flags":{"headerColor":{"state":"ENABLED","variants":{"red":"red","blue":"blue","grey":"grey"},"defaultVariant":"grey", "targeting":{"fractional":[{"var":"email"},["red",50],["blue",50]]}}}}`;
const flagCfg = `{"flags":{"headerColor":{"state":"ENABLED","variants":{"red":"red","blue":"blue","grey":"grey"},"defaultVariant":"grey", "targeting":{"fractional":[{"cat":[{"var":"$flagd.flagKey"},{"var":"email"}]},["red",50],["blue",50]]}}}}`;
core.setConfigurations(flagCfg);

const resolved = core.resolveStringEvaluation('headerColor', 'grey', { email: 'user@openfeature.dev' }, console);
Expand All @@ -206,7 +206,7 @@ describe('flagd-core common flag definitions', () => {

it('should support nested fractional logic', () => {
const core = new FlagdCore();
const flagCfg = `{"flags":{"headerColor":{"state":"ENABLED","variants":{"red":"red","blue":"blue","grey":"grey"},"defaultVariant":"grey", "targeting":{"if":[true,{"fractional":[{"var":"email"},["red",50],["blue",50]]}]}}}}`;
const flagCfg = `{"flags":{"headerColor":{"state":"ENABLED","variants":{"red":"red","blue":"blue","grey":"grey"},"defaultVariant":"grey", "targeting":{"if":[true,{"fractional":[{"cat":[{"var":"$flagd.flagKey"},{"var":"email"}]},["red",50],["blue",50]]}]}}}}`;
core.setConfigurations(flagCfg);

const resolved = core.resolveStringEvaluation('headerColor', 'grey', { email: 'user@openfeature.dev' }, console);
Expand Down
7 changes: 3 additions & 4 deletions libs/shared/flagd-core/src/lib/targeting/fractional.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { flagdPropertyKey, flagKeyPropertyKey, targetingPropertyKey } from './common';
import { flagKeyPropertyKey, flagdPropertyKey, targetingPropertyKey } from './common';
import MurmurHash3 from 'imurmurhash';
import type { EvaluationContext, EvaluationContextValue, Logger } from '@openfeature/core';

Expand Down Expand Up @@ -29,7 +29,7 @@ export function fractionalFactory(logger: Logger) {
bucketBy = args[0];
buckets = args.slice(1, args.length);
} else {
bucketBy = context[targetingPropertyKey];
bucketBy = `${flagdProperties[flagKeyPropertyKey]}${context[targetingPropertyKey]}`;
if (!bucketBy) {
logger.debug('Missing targetingKey property, cannot perform fractional targeting');
return null;
Expand All @@ -47,9 +47,8 @@ export function fractionalFactory(logger: Logger) {
return null;
}

const hashKey = flagdProperties[flagKeyPropertyKey] + bucketBy;
// hash in signed 32 format. Bitwise operation here works in signed 32 hence the conversion
const hash = new MurmurHash3(hashKey).result() | 0;
const hash = new MurmurHash3(bucketBy).result() | 0;
const bucket = (Math.abs(hash) / 2147483648) * 100;

let sum = 0;
Expand Down
4 changes: 2 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 @@ -155,15 +155,15 @@ describe('fractional operator', () => {

it('should evaluate valid rule', () => {
const input = {
fractional: [{ var: 'key' }, ['red', 50], ['blue', 50]],
fractional: [{ cat: [{ var: '$flagd.flagKey' }, { var: 'key' }] }, ['red', 50], ['blue', 50]],
};

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

it('should evaluate valid rule', () => {
const input = {
fractional: [{ var: 'key' }, ['red', 50], ['blue', 50]],
fractional: [{ cat: [{ var: '$flagd.flagKey' }, { var: 'key' }] }, ['red', 50], ['blue', 50]],
};

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

0 comments on commit 6c376b2

Please sign in to comment.