From 6c376b2f525be04c15b5c3bd32d89cc9c4c66729 Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Mon, 15 Apr 2024 11:41:06 -0400 Subject: [PATCH] feat!: allow overrides for fractional seed (#870) Signed-off-by: Michael Beemer Co-authored-by: Todd Baert --- .github/workflows/ci.yml | 8 +-- libs/providers/flagd-web/spec | 2 +- libs/providers/flagd/docker-compose.yaml | 25 +++++++++ libs/providers/flagd/flagd-testbed | 2 +- libs/providers/flagd/spec | 2 +- .../flagd-json-evaluator.spec.ts | 54 ++++++++++--------- .../flagd-core/src/lib/flagd-core.spec.ts | 4 +- .../src/lib/targeting/fractional.ts | 7 ++- .../src/lib/targeting/targeting.spec.ts | 4 +- 9 files changed, 69 insertions(+), 39 deletions(-) create mode 100644 libs/providers/flagd/docker-compose.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b6a6878f..b8a589f80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/libs/providers/flagd-web/spec b/libs/providers/flagd-web/spec index a0b377790..1513e9a02 160000 --- a/libs/providers/flagd-web/spec +++ b/libs/providers/flagd-web/spec @@ -1 +1 @@ -Subproject commit a0b37779091bd2bcce5ba3505ca52c5d26075e67 +Subproject commit 1513e9a0212448a19808bff695180cca9b7b6656 diff --git a/libs/providers/flagd/docker-compose.yaml b/libs/providers/flagd/docker-compose.yaml new file mode 100644 index 000000000..71a14c025 --- /dev/null +++ b/libs/providers/flagd/docker-compose.yaml @@ -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 \ No newline at end of file diff --git a/libs/providers/flagd/flagd-testbed b/libs/providers/flagd/flagd-testbed index a3f34b43d..efcbf72d3 160000 --- a/libs/providers/flagd/flagd-testbed +++ b/libs/providers/flagd/flagd-testbed @@ -1 +1 @@ -Subproject commit a3f34b43de0f9529c554a59d93d4524cbd2de7a7 +Subproject commit efcbf72d34593be47e03ea920b77db29050e47eb diff --git a/libs/providers/flagd/spec b/libs/providers/flagd/spec index b575a8eb2..1513e9a02 160000 --- a/libs/providers/flagd/spec +++ b/libs/providers/flagd/spec @@ -1 +1 @@ -Subproject commit b575a8eb2664a81a17ffd2348c64be89330a4077 +Subproject commit 1513e9a0212448a19808bff695180cca9b7b6656 diff --git a/libs/providers/flagd/src/e2e/step-definitions/flagd-json-evaluator.spec.ts b/libs/providers/flagd/src/e2e/step-definitions/flagd-json-evaluator.spec.ts index 9853107a6..c312ebc9a 100644 --- a/libs/providers/flagd/src/e2e/step-definitions/flagd-json-evaluator.spec.ts +++ b/libs/providers/flagd/src/e2e/step-definitions/flagd-json-evaluator.spec.ts @@ -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 () => { @@ -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); diff --git a/libs/shared/flagd-core/src/lib/flagd-core.spec.ts b/libs/shared/flagd-core/src/lib/flagd-core.spec.ts index 9b077f776..b35dbf964 100644 --- a/libs/shared/flagd-core/src/lib/flagd-core.spec.ts +++ b/libs/shared/flagd-core/src/lib/flagd-core.spec.ts @@ -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); @@ -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); diff --git a/libs/shared/flagd-core/src/lib/targeting/fractional.ts b/libs/shared/flagd-core/src/lib/targeting/fractional.ts index c868831c0..b3d2f04a5 100644 --- a/libs/shared/flagd-core/src/lib/targeting/fractional.ts +++ b/libs/shared/flagd-core/src/lib/targeting/fractional.ts @@ -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'; @@ -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; @@ -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; diff --git a/libs/shared/flagd-core/src/lib/targeting/targeting.spec.ts b/libs/shared/flagd-core/src/lib/targeting/targeting.spec.ts index b161ce884..9cab66d2b 100644 --- a/libs/shared/flagd-core/src/lib/targeting/targeting.spec.ts +++ b/libs/shared/flagd-core/src/lib/targeting/targeting.spec.ts @@ -155,7 +155,7 @@ 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'); @@ -163,7 +163,7 @@ 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: 'bucketKeyB' })).toBe('blue');