diff --git a/lib/shared/bucketing-assembly-script/__tests__/bucketing/bucketing.test.ts b/lib/shared/bucketing-assembly-script/__tests__/bucketing/bucketing.test.ts index f9deed114..e9b553bdd 100644 --- a/lib/shared/bucketing-assembly-script/__tests__/bucketing/bucketing.test.ts +++ b/lib/shared/bucketing-assembly-script/__tests__/bucketing/bucketing.test.ts @@ -234,7 +234,7 @@ describe('Config Parsing and Generating', () => { it('generates the correctly modified config from the example config', () => { const user = { - country: 'canada', + country: 'CA', user_id: 'asuh', email: 'test', } @@ -326,7 +326,7 @@ describe('Config Parsing and Generating', () => { it('puts the user in the target for the first audience they match', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'asuh', customData: { favouriteFood: 'pizza', @@ -544,7 +544,7 @@ describe('Config Parsing and Generating', () => { it('holds user back if not in rollout and passthrough disabled', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'asuh', customData: { favouriteFood: 'pizza', @@ -625,7 +625,7 @@ describe('Config Parsing and Generating', () => { it('pushes user to next target if not in rollout and passthrough not disabled', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'asuh', customData: { favouriteFood: 'pizza', @@ -760,7 +760,7 @@ describe('Config Parsing and Generating', () => { it('pushes user to next target if not in rollout and passthrough setting not defined', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'asuh', customData: { favouriteFood: 'pizza', @@ -906,7 +906,7 @@ describe('Config Parsing and Generating', () => { it('puts user through if in rollout', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'pass_rollout', customData: { favouriteFood: 'pizza', @@ -1035,7 +1035,7 @@ describe('Config Parsing and Generating', () => { it('errors when feature missing distribution', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'asuh', email: 'test@email.com', } @@ -1056,7 +1056,7 @@ describe('Config Parsing and Generating', () => { it('errors when config missing variations', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'pass_rollout', customData: { favouriteFood: 'pizza', @@ -1088,7 +1088,7 @@ describe('Config Parsing and Generating', () => { it('errors when config missing variables', () => { const user = { - country: 'canada', + country: 'CA', user_id: 'asuh', email: 'test@notemail.com', } @@ -1109,7 +1109,7 @@ describe('Config Parsing and Generating', () => { it('puts the user in the target (customData !exists) with null Custom Data', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'asuh', customData: { favouriteFood: 'pizza', @@ -1182,7 +1182,7 @@ describe('Config Parsing and Generating', () => { it('puts the user in the target (customData exists) for the first audience they match', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'asuh', customData: { favouriteFood: 'pizza', @@ -1256,7 +1256,7 @@ describe('Config Parsing and Generating', () => { describe('overrides', () => { it('overrides a bucketing decision as well as a feature that did not pass segmentation', () => { const user = { - country: 'canada', + country: 'CA', user_id: 'asuh', email: 'test', } @@ -1715,7 +1715,7 @@ describe('bucketingKey tests', () => { it('buckets a user with custom bucketingKey', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'pass_rollout', customData: { favouriteFood: 'pizza', @@ -1758,7 +1758,7 @@ describe('bucketingKey tests', () => { it('buckets a user with custom bucketingKey from privateCustomData', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'pass_rollout', privateCustomData: { favouriteFood: 'pizza', @@ -1798,7 +1798,7 @@ describe('bucketingKey tests', () => { it('buckets a user with custom number bucketingKey', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'pass_rollout', privateCustomData: { favouriteNumber: 610, @@ -1836,7 +1836,7 @@ describe('bucketingKey tests', () => { it('buckets a user with custom boolean bucketingKey', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'pass_rollout', privateCustomData: { signed_up: true, diff --git a/lib/shared/bucketing-test-data/src/data/testData.ts b/lib/shared/bucketing-test-data/src/data/testData.ts index 5abc19ec7..ee1a23882 100644 --- a/lib/shared/bucketing-test-data/src/data/testData.ts +++ b/lib/shared/bucketing-test-data/src/data/testData.ts @@ -153,7 +153,7 @@ export const audiences: TargetAudience[] = [ type: FilterType.user, subType: UserSubType.country, comparator: FilterComparator['!='], - values: ['U S AND A'], + values: ['US'], }, ], operator: AudienceOperator.and, @@ -168,7 +168,7 @@ export const audiences: TargetAudience[] = [ type: FilterType.user, subType: UserSubType.country, comparator: FilterComparator['!='], - values: ['U S AND A'], + values: ['US'], }, ], operator: AudienceOperator.and, diff --git a/lib/shared/bucketing/__tests__/bucketing.test.ts b/lib/shared/bucketing/__tests__/bucketing.test.ts index 0b18ebf0c..c400ba61c 100644 --- a/lib/shared/bucketing/__tests__/bucketing.test.ts +++ b/lib/shared/bucketing/__tests__/bucketing.test.ts @@ -1,5 +1,12 @@ /* eslint-disable max-len */ -import { Audience, FeatureType, PublicRollout, Rollout } from '@devcycle/types' +import { + Audience, + EVAL_REASONS, + EvalReason, + FeatureType, + PublicRollout, + Rollout, +} from '@devcycle/types' import { generateBoundedHashes, decideTargetVariation, @@ -22,7 +29,7 @@ import * as uuid from 'uuid' describe('User Hashing and Bucketing', () => { it('generates buckets approximately in the same distribution as the variation distributions', () => { - const buckets: string[] = [] + const buckets: { variation: string; evalReason?: EvalReason }[] = [] const testTarget = { _audience: { _id: 'id', filters: [] } as unknown as Audience, _id: 'target', @@ -49,10 +56,10 @@ describe('User Hashing and Bucketing', () => { ) }) - const var1 = filter(buckets, (bucket) => bucket === 'var1') - const var2 = filter(buckets, (bucket) => bucket === 'var2') - const var3 = filter(buckets, (bucket) => bucket === 'var3') - const var4 = filter(buckets, (bucket) => bucket === 'var4') + const var1 = filter(buckets, (bucket) => bucket.variation === 'var1') + const var2 = filter(buckets, (bucket) => bucket.variation === 'var2') + const var3 = filter(buckets, (bucket) => bucket.variation === 'var3') + const var4 = filter(buckets, (bucket) => bucket.variation === 'var4') expect(var1.length / buckets.length).toBeGreaterThan(0.2525) expect(var1.length / buckets.length).toBeLessThan(0.2575) @@ -107,7 +114,7 @@ describe('User Hashing and Bucketing', () => { describe('Config Parsing and Generating', () => { it('generates the correctly modified config from the example config', () => { const user = { - country: 'canada', + country: 'CA', user_id: 'asuh', email: 'test', platform: 'android', @@ -130,6 +137,10 @@ describe('Config Parsing and Generating', () => { _variation: '615357cf7e9ebdca58446ed0', variationName: 'variation 2', variationKey: 'variation-2-key', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'User ID AND Country', + }, }, }, featureVariationMap: { @@ -143,6 +154,10 @@ describe('Config Parsing and Generating', () => { key: 'swagTest', type: 'String', value: 'YEEEEOWZA', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'User ID AND Country', + }, }, 'bool-var': { _id: '61538237b0a70b58ae6af71y', @@ -150,6 +165,10 @@ describe('Config Parsing and Generating', () => { key: 'bool-var', type: 'Boolean', value: false, + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'User ID AND Country', + }, }, 'json-var': { _id: '61538237b0a70b58ae6af71q', @@ -157,6 +176,10 @@ describe('Config Parsing and Generating', () => { key: 'json-var', type: 'JSON', value: '{"hello":"world","num":610,"bool":true}', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'User ID AND Country', + }, }, 'num-var': { _id: '61538237b0a70b58ae6af71s', @@ -164,6 +187,10 @@ describe('Config Parsing and Generating', () => { key: 'num-var', type: 'Number', value: 610.61, + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'User ID AND Country', + }, }, }, } @@ -173,7 +200,7 @@ describe('Config Parsing and Generating', () => { it('puts the user in the target for the first audience they match', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'asuh', customData: { favouriteFood: 'pizza', @@ -207,6 +234,10 @@ describe('Config Parsing and Generating', () => { _variation: '6153553b8cf4e45e0464268d', variationName: 'variation 1', variationKey: 'variation-1-key', + eval: { + reason: EVAL_REASONS.SPLIT, + details: 'Email', + }, }, feature2: { _id: '614ef6aa475928459060721a', @@ -215,6 +246,10 @@ describe('Config Parsing and Generating', () => { _variation: '615382338424cb11646d7668', variationName: 'feature 2 variation', variationKey: 'variation-feature-2-key', + eval: { + reason: EVAL_REASONS.SPLIT, + details: 'Email', + }, }, feature3: { _id: '614ef6aa475928459060721c', @@ -223,6 +258,10 @@ describe('Config Parsing and Generating', () => { type: 'release', variationKey: 'audience-match-variation', variationName: 'audience match variation', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'Audience Match -> Email', + }, }, feature4: { _id: '614ef8aa475928459060721c', @@ -232,6 +271,10 @@ describe('Config Parsing and Generating', () => { type: 'release', variationKey: 'variation-feature-2-key', variationName: 'feature 4 variation', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'Email', + }, }, }, featureVariationMap: { @@ -248,6 +291,10 @@ describe('Config Parsing and Generating', () => { key: 'audience-match', type: 'String', value: 'audience_match', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'Audience Match -> Email', + }, }, 'feature2.cool': { _id: '61538237b0a70b58ae6af71g', @@ -255,6 +302,10 @@ describe('Config Parsing and Generating', () => { key: 'feature2.cool', type: 'String', value: 'multivar first', + eval: { + reason: EVAL_REASONS.SPLIT, + details: 'Email', + }, }, 'feature2.hello': { _id: '61538237b0a70b58ae6af71h', @@ -262,6 +313,10 @@ describe('Config Parsing and Generating', () => { key: 'feature2.hello', type: 'String', value: 'multivar last', + eval: { + reason: EVAL_REASONS.SPLIT, + details: 'Email', + }, }, swagTest: { _id: '615356f120ed334a6054564c', @@ -269,6 +324,10 @@ describe('Config Parsing and Generating', () => { key: 'swagTest', type: 'String', value: 'man', + eval: { + reason: EVAL_REASONS.SPLIT, + details: 'Email', + }, }, test: { _id: '614ef6ea475129459160721a', @@ -276,6 +335,10 @@ describe('Config Parsing and Generating', () => { key: 'test', type: 'String', value: 'scat', + eval: { + reason: EVAL_REASONS.SPLIT, + details: 'Email', + }, }, 'bool-var': { _id: '61538237b0a70b58ae6af71y', @@ -283,6 +346,10 @@ describe('Config Parsing and Generating', () => { key: 'bool-var', type: 'Boolean', value: false, + eval: { + reason: EVAL_REASONS.SPLIT, + details: 'Email', + }, }, 'json-var': { _id: '61538237b0a70b58ae6af71q', @@ -290,6 +357,10 @@ describe('Config Parsing and Generating', () => { key: 'json-var', type: 'JSON', value: '{"hello":"world","num":610,"bool":true}', + eval: { + reason: EVAL_REASONS.SPLIT, + details: 'Email', + }, }, 'num-var': { _id: '61538237b0a70b58ae6af71s', @@ -297,6 +368,10 @@ describe('Config Parsing and Generating', () => { key: 'num-var', type: 'Number', value: 610.61, + eval: { + reason: EVAL_REASONS.SPLIT, + details: 'Email', + }, }, feature4Var: { _id: '61538937b0a70b58ae6af71f', @@ -304,6 +379,10 @@ describe('Config Parsing and Generating', () => { key: 'feature4Var', type: 'String', value: 'feature 4 value', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'Email', + }, }, }, } @@ -313,7 +392,7 @@ describe('Config Parsing and Generating', () => { it('correctly buckets based on nested filters', () => { const user = { - country: 'Canada', + country: 'CA', user_id: 'asuh', platform: 'Android', email: 'test1@email.com', @@ -337,6 +416,10 @@ describe('Config Parsing and Generating', () => { _variation: '615357cf7e9ebdca58446ed0', variationName: 'variation 2', variationKey: 'variation-2-key', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'User ID AND Country', + }, }, }, featureVariationMap: { @@ -350,6 +433,10 @@ describe('Config Parsing and Generating', () => { key: 'swagTest', type: 'String', value: 'YEEEEOWZA', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'User ID AND Country', + }, }, 'bool-var': { _id: '61538237b0a70b58ae6af71y', @@ -357,6 +444,10 @@ describe('Config Parsing and Generating', () => { key: 'bool-var', type: 'Boolean', value: false, + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'User ID AND Country', + }, }, 'json-var': { _id: '61538237b0a70b58ae6af71q', @@ -364,6 +455,10 @@ describe('Config Parsing and Generating', () => { key: 'json-var', type: 'JSON', value: '{"hello":"world","num":610,"bool":true}', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'User ID AND Country', + }, }, 'num-var': { _id: '61538237b0a70b58ae6af71s', @@ -371,6 +466,10 @@ describe('Config Parsing and Generating', () => { key: 'num-var', type: 'Number', value: 610.61, + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'User ID AND Country', + }, }, }, } @@ -380,7 +479,7 @@ describe('Config Parsing and Generating', () => { it('correctly doesnt bucket with nested filters', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'asuh', platform: 'Android', email: 'notthisemail@email.com', @@ -401,6 +500,10 @@ describe('Config Parsing and Generating', () => { type: 'release', variationKey: 'variation-1-key', variationName: 'variation 1', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'All Users', + }, }, }, project: { @@ -422,6 +525,10 @@ describe('Config Parsing and Generating', () => { key: 'bool-var', type: 'Boolean', value: false, + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'All Users', + }, }, 'json-var': { _id: '61538237b0a70b58ae6af71q', @@ -429,6 +536,10 @@ describe('Config Parsing and Generating', () => { key: 'json-var', type: 'JSON', value: '{"hello":"world","num":610,"bool":true}', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'All Users', + }, }, 'num-var': { _id: '61538237b0a70b58ae6af71s', @@ -436,6 +547,10 @@ describe('Config Parsing and Generating', () => { key: 'num-var', type: 'Number', value: 610.61, + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'All Users', + }, }, swagTest: { _id: '615356f120ed334a6054564c', @@ -443,6 +558,10 @@ describe('Config Parsing and Generating', () => { key: 'swagTest', type: 'String', value: 'man', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'All Users', + }, }, test: { _id: '614ef6ea475129459160721a', @@ -450,6 +569,10 @@ describe('Config Parsing and Generating', () => { key: 'test', type: 'String', value: 'scat', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'All Users', + }, }, }, } @@ -469,7 +592,7 @@ describe('Config Parsing and Generating', () => { }, } const user = { - country: 'U S AND A', + country: 'US', user_id: 'asuh', customData: { favouriteFood: 'pizza', @@ -503,6 +626,11 @@ describe('Config Parsing and Generating', () => { _variation: '615382338424cb11646d7667', variationName: 'variation 1 aud 2', variationKey: 'variation-1-aud-2-key', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: + 'Platform Version AND Custom Data -> favouriteFood AND Custom Data -> favouriteDrink', + }, }, }, featureVariationMap: { @@ -516,6 +644,11 @@ describe('Config Parsing and Generating', () => { key: 'feature2Var', type: 'String', value: 'Var 1 aud 2', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: + 'Platform Version AND Custom Data -> favouriteFood AND Custom Data -> favouriteDrink', + }, }, }, } @@ -525,7 +658,7 @@ describe('Config Parsing and Generating', () => { it('pushes user to next target if not in rollout and passthrough undefined', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'asuh', customData: { favouriteFood: 'pizza', @@ -572,6 +705,10 @@ describe('Config Parsing and Generating', () => { type: 'release', variationKey: 'variation-1-key', variationName: 'variation 1', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'All Users', + }, }, feature2: { _id: '614ef6aa475928459060721a', @@ -580,6 +717,11 @@ describe('Config Parsing and Generating', () => { _variation: '615382338424cb11646d7667', variationName: 'variation 1 aud 2', variationKey: 'variation-1-aud-2-key', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: + 'Platform Version AND Custom Data -> favouriteFood AND Custom Data -> favouriteDrink', + }, }, }, featureVariationMap: { @@ -594,6 +736,11 @@ describe('Config Parsing and Generating', () => { key: 'feature2Var', type: 'String', value: 'Var 1 aud 2', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: + 'Platform Version AND Custom Data -> favouriteFood AND Custom Data -> favouriteDrink', + }, }, 'bool-var': { _id: '61538237b0a70b58ae6af71y', @@ -601,6 +748,10 @@ describe('Config Parsing and Generating', () => { key: 'bool-var', type: 'Boolean', value: false, + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'All Users', + }, }, 'json-var': { _id: '61538237b0a70b58ae6af71q', @@ -608,6 +759,10 @@ describe('Config Parsing and Generating', () => { key: 'json-var', type: 'JSON', value: '{"hello":"world","num":610,"bool":true}', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'All Users', + }, }, 'num-var': { _id: '61538237b0a70b58ae6af71s', @@ -615,6 +770,10 @@ describe('Config Parsing and Generating', () => { key: 'num-var', type: 'Number', value: 610.61, + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'All Users', + }, }, swagTest: { _id: '615356f120ed334a6054564c', @@ -622,6 +781,10 @@ describe('Config Parsing and Generating', () => { key: 'swagTest', type: 'String', value: 'man', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'All Users', + }, }, test: { _id: '614ef6ea475129459160721a', @@ -629,6 +792,10 @@ describe('Config Parsing and Generating', () => { key: 'test', type: 'String', value: 'scat', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'All Users', + }, }, }, } @@ -638,7 +805,7 @@ describe('Config Parsing and Generating', () => { it('pushes user to next target if not in rollout', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'asuh', customData: { favouriteFood: 'pizza', @@ -673,6 +840,10 @@ describe('Config Parsing and Generating', () => { type: 'release', variationKey: 'variation-1-key', variationName: 'variation 1', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'All Users', + }, }, feature2: { _id: '614ef6aa475928459060721a', @@ -681,6 +852,11 @@ describe('Config Parsing and Generating', () => { _variation: '615382338424cb11646d7667', variationName: 'variation 1 aud 2', variationKey: 'variation-1-aud-2-key', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: + 'Platform Version AND Custom Data -> favouriteFood AND Custom Data -> favouriteDrink', + }, }, }, featureVariationMap: { @@ -695,6 +871,11 @@ describe('Config Parsing and Generating', () => { key: 'feature2Var', type: 'String', value: 'Var 1 aud 2', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: + 'Platform Version AND Custom Data -> favouriteFood AND Custom Data -> favouriteDrink', + }, }, 'bool-var': { _id: '61538237b0a70b58ae6af71y', @@ -702,6 +883,10 @@ describe('Config Parsing and Generating', () => { key: 'bool-var', type: 'Boolean', value: false, + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'All Users', + }, }, 'json-var': { _id: '61538237b0a70b58ae6af71q', @@ -709,6 +894,10 @@ describe('Config Parsing and Generating', () => { key: 'json-var', type: 'JSON', value: '{"hello":"world","num":610,"bool":true}', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'All Users', + }, }, 'num-var': { _id: '61538237b0a70b58ae6af71s', @@ -716,6 +905,10 @@ describe('Config Parsing and Generating', () => { key: 'num-var', type: 'Number', value: 610.61, + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'All Users', + }, }, swagTest: { _id: '615356f120ed334a6054564c', @@ -723,6 +916,10 @@ describe('Config Parsing and Generating', () => { key: 'swagTest', type: 'String', value: 'man', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'All Users', + }, }, test: { _id: '614ef6ea475129459160721a', @@ -730,6 +927,10 @@ describe('Config Parsing and Generating', () => { key: 'test', type: 'String', value: 'scat', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'All Users', + }, }, }, } @@ -739,7 +940,7 @@ describe('Config Parsing and Generating', () => { it('puts user through if in rollout', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'pass_rollout', customData: { favouriteFood: 'pizza', @@ -769,6 +970,11 @@ describe('Config Parsing and Generating', () => { _variation: '615357cf7e9ebdca58446ed0', variationName: 'variation 2', variationKey: 'variation-2-key', + eval: { + reason: EVAL_REASONS.SPLIT, + details: + 'Platform Version AND Custom Data -> favouriteFood AND Custom Data -> favouriteDrink', + }, }, feature2: { _id: '614ef6aa475928459060721a', @@ -777,6 +983,11 @@ describe('Config Parsing and Generating', () => { _variation: '615382338424cb11646d7667', variationName: 'variation 1 aud 2', variationKey: 'variation-1-aud-2-key', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: + 'Platform Version AND Custom Data -> favouriteFood AND Custom Data -> favouriteDrink', + }, }, }, featureVariationMap: { @@ -791,6 +1002,11 @@ describe('Config Parsing and Generating', () => { key: 'swagTest', type: 'String', value: 'YEEEEOWZA', + eval: { + reason: EVAL_REASONS.SPLIT, + details: + 'Platform Version AND Custom Data -> favouriteFood AND Custom Data -> favouriteDrink', + }, }, feature2Var: { _id: '61538237b0a70b58ae6af71f', @@ -798,6 +1014,11 @@ describe('Config Parsing and Generating', () => { key: 'feature2Var', type: 'String', value: 'Var 1 aud 2', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: + 'Platform Version AND Custom Data -> favouriteFood AND Custom Data -> favouriteDrink', + }, }, 'bool-var': { _id: '61538237b0a70b58ae6af71y', @@ -805,6 +1026,11 @@ describe('Config Parsing and Generating', () => { key: 'bool-var', type: 'Boolean', value: false, + eval: { + reason: EVAL_REASONS.SPLIT, + details: + 'Platform Version AND Custom Data -> favouriteFood AND Custom Data -> favouriteDrink', + }, }, 'json-var': { _id: '61538237b0a70b58ae6af71q', @@ -812,6 +1038,11 @@ describe('Config Parsing and Generating', () => { key: 'json-var', type: 'JSON', value: '{"hello":"world","num":610,"bool":true}', + eval: { + reason: EVAL_REASONS.SPLIT, + details: + 'Platform Version AND Custom Data -> favouriteFood AND Custom Data -> favouriteDrink', + }, }, 'num-var': { _id: '61538237b0a70b58ae6af71s', @@ -819,6 +1050,11 @@ describe('Config Parsing and Generating', () => { key: 'num-var', type: 'Number', value: 610.61, + eval: { + reason: EVAL_REASONS.SPLIT, + details: + 'Platform Version AND Custom Data -> favouriteFood AND Custom Data -> favouriteDrink', + }, }, }, } @@ -828,7 +1064,7 @@ describe('Config Parsing and Generating', () => { it('buckets a user with user_id if no bucketingKey', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'pass_rollout', customData: { favouriteFood: 'pizza', @@ -856,7 +1092,7 @@ describe('Config Parsing and Generating', () => { it('buckets a user with custom bucketingKey', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'pass_rollout', customData: { favouriteFood: 'pizza', @@ -910,7 +1146,7 @@ describe('Config Parsing and Generating', () => { it('buckets a user with custom bucketingKey from privateCustomData', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'pass_rollout', privateCustomData: { favouriteFood: 'pizza', @@ -961,7 +1197,7 @@ describe('Config Parsing and Generating', () => { it('buckets a user with custom number bucketingKey', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'pass_rollout', privateCustomData: { favouriteNumber: 610, @@ -1012,7 +1248,7 @@ describe('Config Parsing and Generating', () => { it('buckets a user with custom boolean bucketingKey', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'pass_rollout', privateCustomData: { signed_up: true, @@ -1063,7 +1299,7 @@ describe('Config Parsing and Generating', () => { it('errors when feature missing distribution', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'asuh', email: 'test@email.com', } @@ -1074,7 +1310,7 @@ describe('Config Parsing and Generating', () => { it('errors when config missing variations', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'pass_rollout', customData: { favouriteFood: 'pizza', @@ -1095,7 +1331,7 @@ describe('Config Parsing and Generating', () => { it('errors when config missing variables', () => { const user = { - country: 'canada', + country: 'CA', user_id: 'asuh', email: 'test@notemail.com', platform: 'Android', @@ -1107,7 +1343,7 @@ describe('Config Parsing and Generating', () => { it('puts the user in the target (customData !exists) with null Custom Data', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'asuh', customData: { favouriteFood: 'pizza', @@ -1140,7 +1376,10 @@ describe('Config Parsing and Generating', () => { type: 'ops', variationKey: 'audience-match-variation', variationName: 'audience match variation', - settings: undefined, + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'Custom Data -> favouriteNull', + }, }, }, featureVariationMap: { @@ -1154,6 +1393,10 @@ describe('Config Parsing and Generating', () => { key: 'audience-match', type: 'String', value: 'audience_match', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'Custom Data -> favouriteNull', + }, }, }, } @@ -1170,7 +1413,7 @@ describe('Config Parsing and Generating', () => { it('puts the user in the target (customData exists) for the first audience they match', () => { const user = { - country: 'U S AND A', + country: 'US', user_id: 'asuh', customData: { favouriteFood: 'pizza', @@ -1203,7 +1446,10 @@ describe('Config Parsing and Generating', () => { type: 'permission', variationKey: 'audience-match-variation', variationName: 'audience match variation', - setting: undefined, + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'Custom Data -> favouriteNull', + }, }, }, featureVariationMap: { @@ -1217,6 +1463,10 @@ describe('Config Parsing and Generating', () => { key: 'audience-match', type: 'String', value: 'audience_match', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + details: 'Custom Data -> favouriteNull', + }, }, }, } @@ -1719,7 +1969,7 @@ describe('Rollout Logic', () => { describe('overrides', () => { it('correctly overrides a bucketing decision and a feature that doesnt normally pass segmentation', () => { const user = { - country: 'canada', + country: 'CA', user_id: 'asuh', email: 'test', platform: 'android', @@ -1748,6 +1998,10 @@ describe('Rollout Logic', () => { type: 'release', variationKey: 'variation-1-key', variationName: 'variation 1', + eval: { + reason: EVAL_REASONS.OVERRIDE, + details: 'Override', + }, }, feature2: { _id: '614ef6aa475928459060721a', @@ -1756,6 +2010,10 @@ describe('Rollout Logic', () => { type: 'release', variationKey: 'variation-1-aud-2-key', variationName: 'variation 1 aud 2', + eval: { + reason: EVAL_REASONS.OVERRIDE, + details: 'Override', + }, }, }, featureVariationMap: { @@ -1770,6 +2028,10 @@ describe('Rollout Logic', () => { key: 'swagTest', type: 'String', value: 'man', + eval: { + reason: EVAL_REASONS.OVERRIDE, + details: 'Override', + }, }, 'bool-var': { _id: '61538237b0a70b58ae6af71y', @@ -1777,6 +2039,10 @@ describe('Rollout Logic', () => { key: 'bool-var', type: 'Boolean', value: false, + eval: { + reason: EVAL_REASONS.OVERRIDE, + details: 'Override', + }, }, feature2Var: { _id: '61538237b0a70b58ae6af71f', @@ -1784,6 +2050,10 @@ describe('Rollout Logic', () => { key: 'feature2Var', type: 'String', value: 'Var 1 aud 2', + eval: { + reason: EVAL_REASONS.OVERRIDE, + details: 'Override', + }, }, 'json-var': { _id: '61538237b0a70b58ae6af71q', @@ -1791,6 +2061,10 @@ describe('Rollout Logic', () => { key: 'json-var', type: 'JSON', value: '{"hello":"world","num":610,"bool":true}', + eval: { + reason: EVAL_REASONS.OVERRIDE, + details: 'Override', + }, }, 'num-var': { _id: '61538237b0a70b58ae6af71s', @@ -1798,6 +2072,10 @@ describe('Rollout Logic', () => { key: 'num-var', type: 'Number', value: 610.61, + eval: { + reason: EVAL_REASONS.OVERRIDE, + details: 'Override', + }, }, test: { _id: '614ef6ea475129459160721a', @@ -1805,6 +2083,10 @@ describe('Rollout Logic', () => { key: 'test', type: 'String', value: 'scat', + eval: { + reason: EVAL_REASONS.OVERRIDE, + details: 'Override', + }, }, }, } diff --git a/lib/shared/bucketing/__tests__/segmentation.test.ts b/lib/shared/bucketing/__tests__/segmentation.test.ts index ac90bb218..e925688f4 100644 --- a/lib/shared/bucketing/__tests__/segmentation.test.ts +++ b/lib/shared/bucketing/__tests__/segmentation.test.ts @@ -206,13 +206,13 @@ describe('SegmentationManager Unit Test', () => { } const data = { - country: 'Canada', + country: 'CA', email: 'brooks@big.lunch', platformVersion: '2.0.0', platform: 'iOS', } - assert.strictEqual( - false, + assert.deepStrictEqual( + { result: false }, segmentation.evaluateOperator({ data, operator, @@ -224,8 +224,8 @@ describe('SegmentationManager Unit Test', () => { filters, operator: AudienceOperator.or, } - assert.strictEqual( - false, + assert.deepStrictEqual( + { result: false }, segmentation.evaluateOperator({ data: {}, operator: orOp, @@ -239,8 +239,6 @@ describe('SegmentationManager Unit Test', () => { const filters = [ { type: 'all', - comparator: '=', - values: [], }, ] as AudienceFilterOrOperator[] @@ -250,13 +248,13 @@ describe('SegmentationManager Unit Test', () => { } const data = { - country: 'Canada', + country: 'CA', email: 'brooks@big.lunch', platformVersion: '2.0.0', platform: 'iOS', } - assert.strictEqual( - true, + assert.deepStrictEqual( + { result: true, reasonDetails: 'All Users' }, segmentation.evaluateOperator({ data, operator, @@ -272,7 +270,7 @@ describe('SegmentationManager Unit Test', () => { type: 'user', subType: 'country', comparator: '=', - values: ['Canada'], + values: ['CA'], }, { type: 'user', @@ -294,14 +292,17 @@ describe('SegmentationManager Unit Test', () => { } const data = { - country: 'Canada', + country: 'CA', email: 'brooks@big.lunch', platformVersion: '2.0.0', appVersion: '2.0.2', platform: 'iOS', } - assert.strictEqual( - true, + assert.deepStrictEqual( + { + result: true, + reasonDetails: 'Country AND Email AND App Version', + }, segmentation.evaluateOperator({ data, operator, @@ -317,7 +318,7 @@ describe('SegmentationManager Unit Test', () => { type: 'user', subType: 'country', comparator: '=', - values: ['Canada'], + values: ['CA'], }, { type: 'user', @@ -345,8 +346,8 @@ describe('SegmentationManager Unit Test', () => { appVersion: '2.0.2', platform: 'iOS', } - assert.strictEqual( - true, + assert.deepStrictEqual( + { result: true, reasonDetails: 'App Version' }, segmentation.evaluateOperator({ data, operator, @@ -362,12 +363,13 @@ describe('SegmentationManager Unit Test', () => { type: 'user', subType: 'country', comparator: '=', - values: ['Canada'], + values: ['CA'], }, { type: 'user', subType: 'customData', - datakey: '', + dataKey: 'full_country', + dataKeyType: 'String', comparator: '=', values: ['Canada'], }, @@ -379,13 +381,19 @@ describe('SegmentationManager Unit Test', () => { } const data = { - country: 'Canada', + country: 'CA', email: 'brooks@big.lunch', appVersion: '2.0.0', platform: 'iOS', + customData: { + full_country: 'Canada', + }, } - assert.strictEqual( - true, + assert.deepStrictEqual( + { + result: true, + reasonDetails: 'Country AND Custom Data -> full_country', + }, segmentation.evaluateOperator({ data, operator, @@ -409,8 +417,8 @@ describe('SegmentationManager Unit Test', () => { } as TopLevelOperator const data = { user_id: 'test_user' } - assert.strictEqual( - true, + assert.deepStrictEqual( + { result: true, reasonDetails: 'User ID' }, segmentation.evaluateOperator({ data, operator, @@ -434,8 +442,8 @@ describe('SegmentationManager Unit Test', () => { } as TopLevelOperator const data = { email: 'test@devcycle.com' } - assert.strictEqual( - true, + assert.deepStrictEqual( + { result: true, reasonDetails: 'Email' }, segmentation.evaluateOperator({ data, operator, @@ -459,8 +467,8 @@ describe('SegmentationManager Unit Test', () => { } as TopLevelOperator const data = { country: 'CA' } - assert.strictEqual( - true, + assert.deepStrictEqual( + { result: true, reasonDetails: 'Country' }, segmentation.evaluateOperator({ data, operator, @@ -484,8 +492,8 @@ describe('SegmentationManager Unit Test', () => { } as TopLevelOperator const data = { appVersion: '1.0.1' } - assert.strictEqual( - true, + assert.deepStrictEqual( + { result: true, reasonDetails: 'App Version' }, segmentation.evaluateOperator({ data, operator, @@ -509,8 +517,8 @@ describe('SegmentationManager Unit Test', () => { } as TopLevelOperator const data = { platformVersion: '15.1' } - assert.strictEqual( - true, + assert.deepStrictEqual( + { result: true, reasonDetails: 'Platform Version' }, segmentation.evaluateOperator({ data, operator, @@ -534,8 +542,8 @@ describe('SegmentationManager Unit Test', () => { } as TopLevelOperator const data = { platform: 'iPadOS' } - assert.strictEqual( - true, + assert.deepStrictEqual( + { result: true, reasonDetails: 'Platform' }, segmentation.evaluateOperator({ data, operator, @@ -559,8 +567,8 @@ describe('SegmentationManager Unit Test', () => { } as TopLevelOperator const data = { deviceModel: 'Samsung Galaxy F12' } - assert.strictEqual( - true, + assert.deepStrictEqual( + { result: true, reasonDetails: 'Device Model' }, segmentation.evaluateOperator({ data, operator, @@ -586,8 +594,8 @@ describe('SegmentationManager Unit Test', () => { } as TopLevelOperator const data = { customData: { testKey: 'dataValue' } } - assert.strictEqual( - true, + assert.deepStrictEqual( + { result: true, reasonDetails: 'Custom Data -> testKey' }, segmentation.evaluateOperator({ data, operator, @@ -613,8 +621,8 @@ describe('SegmentationManager Unit Test', () => { } as TopLevelOperator const data = { customData: { testKey: 'dataValue' } } - assert.strictEqual( - false, + assert.deepStrictEqual( + { result: false }, segmentation.evaluateOperator({ data, operator, @@ -640,8 +648,8 @@ describe('SegmentationManager Unit Test', () => { } as TopLevelOperator const data = { privateCustomData: { testKey: 'dataValue' } } - assert.strictEqual( - false, + assert.deepStrictEqual( + { result: false }, segmentation.evaluateOperator({ data, operator, @@ -667,8 +675,8 @@ describe('SegmentationManager Unit Test', () => { } as TopLevelOperator const data = { customData: { testKey: 'otherValue' } } - assert.strictEqual( - false, + assert.deepStrictEqual( + { result: false }, segmentation.evaluateOperator({ data, operator, @@ -683,15 +691,13 @@ describe('SegmentationManager Unit Test', () => { filters: [ { type: 'optIn', - comparator: '=', - values: [], }, ] as AudienceFilterOrOperator[], operator: 'and', } as TopLevelOperator const optInData = { - country: 'Canada', + country: 'CA', email: 'brooks@big.lunch', platformVersion: '2.0.0', platform: 'iOS', @@ -701,8 +707,8 @@ describe('SegmentationManager Unit Test', () => { } it('should pass optIn filter when feature in optIns and isOptInEnabled', () => { - assert.strictEqual( - true, + assert.deepStrictEqual( + { result: true, reasonDetails: 'Opt-In' }, segmentation.evaluateOperator({ data: optInData, operator: optInOperator, @@ -713,8 +719,8 @@ describe('SegmentationManager Unit Test', () => { }) it('should fail optIn filter when feature in optIns but isOptInEnabled is false', () => { - assert.strictEqual( - false, + assert.deepStrictEqual( + { result: false }, segmentation.evaluateOperator({ data: optInData, operator: optInOperator, @@ -725,8 +731,8 @@ describe('SegmentationManager Unit Test', () => { }) it('should fail optIn filter when feature not in optIns', () => { - assert.strictEqual( - false, + assert.deepStrictEqual( + { result: false }, segmentation.evaluateOperator({ data: optInData, operator: optInOperator, @@ -743,7 +749,7 @@ describe('SegmentationManager Unit Test', () => { type: FilterType.user, subType: UserSubType.country, comparator: FilterComparator['='], - values: ['Canada'], + values: ['CA'], }, { type: FilterType.user, @@ -763,7 +769,7 @@ describe('SegmentationManager Unit Test', () => { operator: AudienceOperator.and, } const data = { - country: 'Canada', + country: 'CA', email: 'brooks@big.lunch', platformVersion: '2.0.0', appVersion: '2.0.2', @@ -796,8 +802,12 @@ describe('SegmentationManager Unit Test', () => { filters: operator, }, } - assert.strictEqual( - true, + assert.deepStrictEqual( + { + result: true, + reasonDetails: + 'Audience Match -> Country AND Email AND App Version', + }, segmentation.evaluateOperator({ data, operator: audienceMatchOperator, @@ -809,8 +819,8 @@ describe('SegmentationManager Unit Test', () => { }) it('should not pass seg for nonexistent audience', () => { - assert.strictEqual( - false, + assert.deepStrictEqual( + { result: false }, segmentation.evaluateOperator({ data, operator: audienceMatchOperator, @@ -827,8 +837,8 @@ describe('SegmentationManager Unit Test', () => { }, } - assert.strictEqual( - false, + assert.deepStrictEqual( + { result: false }, segmentation.evaluateOperator({ data, operator: audienceMatchOperatorNotEqual, @@ -858,8 +868,12 @@ describe('SegmentationManager Unit Test', () => { filters: audienceMatchOperator, }, } - assert.strictEqual( - true, + assert.deepStrictEqual( + { + result: true, + reasonDetails: + 'Audience Match -> Audience Match -> Country AND Email AND App Version', + }, segmentation.evaluateOperator({ data, operator: nestedAudienceMatchOperator, @@ -889,8 +903,8 @@ describe('SegmentationManager Unit Test', () => { filters: audienceMatchOperator, }, } - assert.strictEqual( - false, + assert.deepStrictEqual( + { result: false }, segmentation.evaluateOperator({ data, operator: nestedAudienceMatchOperator, @@ -906,7 +920,7 @@ describe('SegmentationManager Unit Test', () => { type: FilterType.user, subType: UserSubType.country, comparator: FilterComparator['='], - values: ['USA'], + values: ['US'], }, ] @@ -931,8 +945,12 @@ describe('SegmentationManager Unit Test', () => { ], operator: AudienceOperator.and, } - assert.strictEqual( - true, + assert.deepStrictEqual( + { + result: true, + reasonDetails: + 'Audience Match -> Country AND Email AND App Version', + }, segmentation.evaluateOperator({ data, operator: audienceMatchOperatorMultiple, @@ -948,7 +966,7 @@ describe('SegmentationManager Unit Test', () => { type: FilterType.user, subType: UserSubType.country, comparator: FilterComparator['='], - values: ['USA'], + values: ['US'], }, ] @@ -973,8 +991,8 @@ describe('SegmentationManager Unit Test', () => { ], operator: AudienceOperator.and, } - assert.strictEqual( - false, + assert.deepStrictEqual( + { result: false }, segmentation.evaluateOperator({ data, operator: audienceMatchOperatorMultiple, @@ -1320,19 +1338,20 @@ describe('SegmentationManager Unit Test', () => { segmentation.checkStringsFilter('Roku', filter), ) }) + it('should return true if string is equal to multiple filters', () => { const filters = [ { type: 'user', subType: 'country', comparator: '=', - values: ['Canada'], + values: ['CA'], }, { type: 'user', subType: 'country', comparator: '!=', - values: ['Not Canada'], + values: ['US'], }, ] as AudienceFilterOrOperator[] @@ -1341,10 +1360,10 @@ describe('SegmentationManager Unit Test', () => { operator: 'and', } as unknown as TopLevelOperator - assert.strictEqual( - true, + assert.deepStrictEqual( + { result: true, reasonDetails: 'Country AND Country' }, segmentation.evaluateOperator({ - data: { country: 'Canada' }, + data: { country: 'CA' }, operator, featureId, isOptInEnabled, @@ -1358,25 +1377,25 @@ describe('SegmentationManager Unit Test', () => { type: 'user', subType: 'country', comparator: '=', - values: ['Canada'], + values: ['CA'], }, { type: 'user', - subType: 'country', + subType: 'user_id', comparator: '=', - values: ['Not Canada'], + values: ['John'], }, ] as AudienceFilterOrOperator[] const operator = { filters, - operator: 'and', - } as unknown as TopLevelOperator + operator: AudienceOperator.and, + } - assert.strictEqual( - false, + assert.deepStrictEqual( + { result: false }, segmentation.evaluateOperator({ - data: { country: 'Canada' }, + data: { country: 'CA', user_id: 'Sam' }, operator, featureId, isOptInEnabled, @@ -3139,8 +3158,12 @@ describe('SegmentationManager Unit Test', () => { filters: [filterStr, filterNum, filterBool], operator: 'and', } as unknown as TopLevelOperator - assert.strictEqual( - true, + assert.deepStrictEqual( + { + result: true, + reasonDetails: + 'Custom Data -> strKey AND Custom Data -> numKey AND Custom Data -> boolKey', + }, segmentation.evaluateOperator({ data: { customData: { @@ -3160,8 +3183,8 @@ describe('SegmentationManager Unit Test', () => { filters: [filterStr, filterNum, filterBool], operator: 'and', } as unknown as TopLevelOperator - assert.strictEqual( - false, + assert.deepStrictEqual( + { result: false }, segmentation.evaluateOperator({ data: { customData: { strKey: 'value', boolKey: false } }, operator: operatorFilter, @@ -3178,8 +3201,12 @@ describe('SegmentationManager Unit Test', () => { filters: [filterStr, filter, filterBool], operator: 'and', } as unknown as TopLevelOperator - assert.strictEqual( - true, + assert.deepStrictEqual( + { + result: true, + reasonDetails: + 'Custom Data -> strKey AND Custom Data -> numKey AND Custom Data -> boolKey', + }, segmentation.evaluateOperator({ data: { customData: { strKey: 'value', boolKey: false } }, operator: operatorFilter, @@ -3211,8 +3238,8 @@ describe('SegmentationManager Unit Test', () => { filters: [filterStr, filter, filterBool], operator: 'and', } as unknown as TopLevelOperator - assert.strictEqual( - false, + assert.deepStrictEqual( + { result: false }, segmentation.evaluateOperator({ data: { customData: null }, operator: operatorFilter, @@ -3229,8 +3256,8 @@ describe('SegmentationManager Unit Test', () => { filters: [filterStr, filter, filterBool], operator: 'and', } as unknown as TopLevelOperator - assert.strictEqual( - false, + assert.deepStrictEqual( + { result: false }, segmentation.evaluateOperator({ data: { customData: null }, operator: operatorFilter, @@ -3612,17 +3639,19 @@ describe('SegmentationManager Unit Test', () => { }, }, ] as unknown as Audience[] + it('should filter all Android TV audiences properly if it is included in data', () => { const data = { platform: 'Android TV', } const filteredAudiences = audiences.filter((aud) => { - return segmentation.evaluateOperator({ + const { result } = segmentation.evaluateOperator({ operator: aud.filters, data, featureId, isOptInEnabled, }) + return result }) expect(filteredAudiences.length).toEqual(3) expect(filteredAudiences[0]._id).toEqual('60cca1d8230f17002542b909') @@ -3634,12 +3663,13 @@ describe('SegmentationManager Unit Test', () => { platform: 'iOS', } const filteredAudiences = audiences.filter((aud) => { - return segmentation.evaluateOperator({ + const { result } = segmentation.evaluateOperator({ operator: aud.filters, data, featureId, isOptInEnabled, }) + return result }) expect(filteredAudiences.length).toEqual(1) expect(filteredAudiences[0]._id).toEqual('60cca1d8230f17002542b913') diff --git a/lib/shared/bucketing/src/bucketing.ts b/lib/shared/bucketing/src/bucketing.ts index b57d51073..feeb6e83a 100644 --- a/lib/shared/bucketing/src/bucketing.ts +++ b/lib/shared/bucketing/src/bucketing.ts @@ -13,11 +13,19 @@ import { PublicRolloutStage, PublicTarget, Variation, + EVAL_REASONS, + EvalReason, + EVAL_REASON_DETAILS, } from '@devcycle/types' import murmurhash from 'murmurhash' import { evaluateOperator } from './segmentation' +type VariationReasonResult = { + variation: string + eval?: EvalReason +} + // Max value of an unsigned 32-bit integer, which is what murmurhash returns const MAX_HASH_VALUE = 4294967295 const baseSeed = 1 @@ -53,11 +61,15 @@ export const generateBoundedHash = ( export const decideTargetVariation = ({ target, boundedHash, + reasonDetails, }: { target: PublicTarget boundedHash: number -}): string => { + reasonDetails?: string +}): VariationReasonResult => { const variations = orderBy(target.distribution, '_variation', ['desc']) + const isRollout = target.rollout !== undefined + const isRandomDistribution = target.distribution.length !== 1 let distributionIndex = 0 const previousDistributionIndex = 0 @@ -67,7 +79,19 @@ export const decideTargetVariation = ({ boundedHash >= previousDistributionIndex && boundedHash < distributionIndex ) { - return variation._variation + return { + variation: variation._variation, + eval: + isRollout || isRandomDistribution + ? { + reason: EVAL_REASONS.SPLIT, + details: reasonDetails ?? undefined, + } + : { + reason: EVAL_REASONS.TARGETING_MATCH, + details: reasonDetails ?? undefined, + }, + } } } throw new Error('Failed to decide target variation') @@ -143,16 +167,19 @@ export const doesUserPassRollout = ({ export const bucketForSegmentedFeature = ({ boundedHash, target, + reasonDetails, }: { target: PublicTarget boundedHash: number -}): string => { - return decideTargetVariation({ target, boundedHash }) + reasonDetails?: string +}): VariationReasonResult => { + return decideTargetVariation({ target, boundedHash, reasonDetails }) } type SegmentedFeatureData = { feature: PublicFeature target: PublicTarget + reasonDetails?: string } const checkRolloutAndEvaluate = ({ @@ -195,21 +222,25 @@ export const getSegmentedFeatureDataFromConfig = ({ feature.settings?.['optInEnabled'] && config.project.settings?.['optIn']?.['enabled'] + let featureReasonDetails = '' const segmentedFeatureTarget = feature.configuration.targets.find( (target) => { + const { result, reasonDetails } = evaluateOperator({ + operator: target._audience.filters, + data: user, + featureId: feature._id, + isOptInEnabled: !!isOptInEnabled, + audiences: config.audiences, + }) + if (result && reasonDetails) { + featureReasonDetails = reasonDetails + } return ( checkRolloutAndEvaluate({ target, user, disablePassthroughRollouts, - }) && - evaluateOperator({ - operator: target._audience.filters, - data: user, - featureId: feature._id, - isOptInEnabled: !!isOptInEnabled, - audiences: config.audiences, - }) + }) && result ) }, ) @@ -217,6 +248,7 @@ export const getSegmentedFeatureDataFromConfig = ({ accumulator.push({ feature, target: segmentedFeatureTarget, + reasonDetails: featureReasonDetails, }) } return accumulator @@ -244,9 +276,11 @@ export const generateBucketedConfig = ({ const updateMapsWithBucketedFeature = ({ feature, variation, + evalReason, }: { feature: Feature variation: Variation + evalReason?: EvalReason }) => { const { _id, key, type, settings } = feature @@ -258,6 +292,7 @@ export const generateBucketedConfig = ({ variationName: variation.name, variationKey: variation.key, settings, + eval: evalReason, } featureVariationMap[_id] = variation._id variation.variables.forEach(({ _var, value }) => { @@ -269,11 +304,12 @@ export const generateBucketedConfig = ({ ...variable, _feature: _id, value, + eval: evalReason, } }) } - segmentedFeatures.forEach(({ feature, target }) => { + segmentedFeatures.forEach(({ feature, target, reasonDetails }) => { const { variations } = feature const bucketingValue = getUserValueForBucketingKey({ user, target }) const { rolloutHash, bucketingHash } = generateBoundedHashes( @@ -291,16 +327,18 @@ export const generateBucketedConfig = ({ return } - const variation_id = bucketForSegmentedFeature({ - boundedHash: bucketingHash, - target, - }) + const { variation: variation_id, eval: evalReason } = + bucketForSegmentedFeature({ + boundedHash: bucketingHash, + target, + reasonDetails, + }) const variation = variations.find((v) => v._id === variation_id) if (!variation) { throw new Error(`Config missing variation: ${variation_id}`) } - updateMapsWithBucketedFeature({ feature, variation }) + updateMapsWithBucketedFeature({ feature, variation, evalReason }) }) for (const [_feature, _variation] of Object.entries(overrides || {})) { @@ -314,7 +352,14 @@ export const generateBucketedConfig = ({ continue } - updateMapsWithBucketedFeature({ feature, variation }) + updateMapsWithBucketedFeature({ + feature, + variation, + evalReason: { + reason: EVAL_REASONS.OVERRIDE, + details: EVAL_REASON_DETAILS.OVERRIDE, + }, + }) } return { diff --git a/lib/shared/bucketing/src/segmentation.ts b/lib/shared/bucketing/src/segmentation.ts index 4af7441d4..403de9a62 100644 --- a/lib/shared/bucketing/src/segmentation.ts +++ b/lib/shared/bucketing/src/segmentation.ts @@ -9,16 +9,21 @@ import { PublicAudience, AudienceFilterOrOperator, UserSubType, + EVAL_REASON_DETAILS, } from '@devcycle/types' import UAParser from 'ua-parser-js' -// TODO add support for OR/XOR as well as recursive filters +type SegmentationResult = { + result: boolean + reasonDetails?: string +} + /** * Evaluate an operator object based on its contained filters and the user data given * Returns true if the user's data allows them through the segmentation - * @param operator - The set of filters to evaluate, and the boolean operator to follow (AND, OR, XOR) + * @param operator - The set of filters to evaluate, and the boolean operator to follow (AND, OR) * @param data - The incoming user, device, and user agent data - * @returns {*|boolean|boolean} + * @returns {SegmentationResult} */ export const evaluateOperator = ({ operator, @@ -32,32 +37,48 @@ export const evaluateOperator = ({ featureId: string isOptInEnabled: boolean audiences?: { [id: string]: Omit, '_id'> } -}): boolean => { - if (!operator?.filters?.length) return false +}): SegmentationResult => { + if (!operator?.filters?.length) return { result: false } - const doesUserPassFilter = (filter: AudienceFilterOrOperator) => { + let reason = '' + const doesUserPassFilter = ( + filter: AudienceFilterOrOperator, + index: number, + ) => { if (filter.operator) { - return evaluateOperator({ + const { result } = evaluateOperator({ operator: filter, data, featureId, isOptInEnabled, audiences, }) + return result + } + if (filter.type === 'all') { + reason = EVAL_REASON_DETAILS.ALL_USERS + return true } - if (filter.type === 'all') return true if (filter.type === 'optIn') { const optIns = data.optIns - return isOptInEnabled && !!optIns?.[featureId] + const result = isOptInEnabled && !!optIns?.[featureId] + reason = result + ? EVAL_REASON_DETAILS.OPT_IN + : EVAL_REASON_DETAILS.NOT_OPTED_IN + return result } if (filter.type === 'audienceMatch') { - return filterForAudienceMatch({ + const { result, reasonDetails } = filterForAudienceMatch({ operator: filter, data, featureId, isOptInEnabled, audiences, }) + if (result && reasonDetails) { + reason = reasonDetails + } + return result } if (filter.type !== 'user') { console.error(`Invalid filter type: ${filter.type}`) @@ -73,20 +94,50 @@ export const evaluateOperator = ({ return false } - return filterFunctionsBySubtype[filter.subType](data, filter) + const { result, reasonDetails } = filterFunctionsBySubtype[ + filter.subType + ](data, filter) + + if (result && reasonDetails) { + if (operator.operator === 'and') { + reason = reason.concat( + reasonDetails, + index < (operator.filters?.length ?? 0) - 1 ? ' AND ' : '', + ) + } else { + reason = reasonDetails + } + } + + return result } if (operator.operator === 'or') { - return operator.filters.some(doesUserPassFilter) + const result = operator.filters.some(doesUserPassFilter) + if (result) { + return { + result, + reasonDetails: reason, + } + } + return { result } } else { - return operator.filters.every(doesUserPassFilter) + const result = operator.filters.every(doesUserPassFilter) + if (result) { + return { + result, + reasonDetails: reason, + } + } + return { result } } } + type FilterFunctionsBySubtype = { [key in UserSubType]: ( data: any, filter: AudienceFilterOrOperator, - ) => boolean + ) => SegmentationResult } function filterForAudienceMatch({ @@ -101,8 +152,8 @@ function filterForAudienceMatch({ featureId: string isOptInEnabled: boolean audiences?: { [id: string]: Omit, '_id'> } -}): boolean { - if (!operator?._audiences) return false +}): SegmentationResult { + if (!operator?._audiences) return { result: false } const comparator = operator.comparator // Recursively evaluate every audience in the _audiences array for (const _audience of operator._audiences) { @@ -112,39 +163,96 @@ function filterForAudienceMatch({ console.error( 'Invalid audience referenced by audienceMatch filter.', ) - return false + return { result: false } } - if ( - evaluateOperator({ - operator: audience.filters, - data, - featureId, - isOptInEnabled, - audiences, - }) - ) { - return comparator === '=' + const { result, reasonDetails } = evaluateOperator({ + operator: audience.filters, + data, + featureId, + isOptInEnabled, + audiences, + }) + if (result) { + return { + result: comparator === '=', + reasonDetails: + EVAL_REASON_DETAILS.AUDIENCE_MATCH + + (reasonDetails ? ` -> ${reasonDetails}` : ''), + } } } - return comparator === '!=' + return { + result: comparator === '!=', + reasonDetails: EVAL_REASON_DETAILS.NOT_IN_AUDIENCE, + } } const filterFunctionsBySubtype: FilterFunctionsBySubtype = { - country: (data, filter) => checkStringsFilter(data.country, filter), - email: (data, filter) => checkStringsFilter(data.email, filter), - ip: (data, filter) => checkStringsFilter(data.ip, filter), - user_id: (data, filter) => checkStringsFilter(data.user_id, filter), - appVersion: (data, filter) => checkVersionFilters(data.appVersion, filter), - platformVersion: (data, filter) => - checkVersionFilters(data.platformVersion, filter), - deviceModel: (data, filter) => checkStringsFilter(data.deviceModel, filter), - platform: (data, filter) => checkStringsFilter(data.platform, filter), + country: (data, filter) => { + const result = checkStringsFilter(data.country, filter) + return { + result, + reasonDetails: result ? EVAL_REASON_DETAILS.COUNTRY : undefined, + } + }, + email: (data, filter) => { + const result = checkStringsFilter(data.email, filter) + return { + result, + reasonDetails: result ? EVAL_REASON_DETAILS.EMAIL : undefined, + } + }, + user_id: (data, filter) => { + const result = checkStringsFilter(data.user_id, filter) + return { + result, + reasonDetails: result ? EVAL_REASON_DETAILS.USER_ID : undefined, + } + }, + appVersion: (data, filter) => { + const result = checkVersionFilters(data.appVersion, filter) + return { + result, + reasonDetails: result ? EVAL_REASON_DETAILS.APP_VERSION : undefined, + } + }, + platformVersion: (data, filter) => { + const result = checkVersionFilters(data.platformVersion, filter) + return { + result, + reasonDetails: result + ? EVAL_REASON_DETAILS.PLATFORM_VERSION + : undefined, + } + }, + deviceModel: (data, filter) => { + const result = checkStringsFilter(data.deviceModel, filter) + return { + result, + reasonDetails: result + ? EVAL_REASON_DETAILS.DEVICE_MODEL + : undefined, + } + }, + platform: (data, filter) => { + const result = checkStringsFilter(data.platform, filter) + return { + result, + reasonDetails: result ? EVAL_REASON_DETAILS.PLATFORM : undefined, + } + }, customData: (data, filter) => { const combinedCustomData = { ...data.customData, ...data.privateCustomData, } - return checkCustomData(combinedCustomData, filter) + const result = checkCustomData(combinedCustomData, filter) + return { + result, + reasonDetails: result + ? EVAL_REASON_DETAILS.CUSTOM_DATA + ` -> ${filter.dataKey}` + : undefined, + } }, } diff --git a/lib/shared/types/src/types/apis/sdk/clientSDKAPI.ts b/lib/shared/types/src/types/apis/sdk/clientSDKAPI.ts index 42a86c1c4..4e9720959 100644 --- a/lib/shared/types/src/types/apis/sdk/clientSDKAPI.ts +++ b/lib/shared/types/src/types/apis/sdk/clientSDKAPI.ts @@ -27,6 +27,57 @@ export type SDKTypes = (typeof SDKTypeValues)[number] export type QueryParams = { [key: string]: string } +export enum EVAL_REASONS { + TARGETING_MATCH = 'TARGETING_MATCH', + SPLIT = 'SPLIT', + DEFAULT = 'DEFAULT', + DISABLED = 'DISABLED', + ERROR = 'ERROR', + OVERRIDE = 'OVERRIDE', + OPT_IN = 'OPT_IN', +} + +export enum EVAL_REASON_DETAILS { + // All Users + ALL_USERS = 'All Users', + // Audiences + AUDIENCE_MATCH = 'Audience Match', + NOT_IN_AUDIENCE = 'Not in Audience', + // Opt-In + OPT_IN = 'Opt-In', + NOT_OPTED_IN = 'Not Opt-In', + // Overrides + OVERRIDE = 'Override', + // User Specific + USER_ID = 'User ID', + EMAIL = 'Email', + COUNTRY = 'Country', + PLATFORM = 'Platform', + PLATFORM_VERSION = 'Platform Version', + APP_VERSION = 'App Version', + DEVICE_MODEL = 'Device Model', + CUSTOM_DATA = 'Custom Data', +} + +export enum DEFAULT_REASON_DETAILS { + MISSING_CONFIG = 'Missing Config', + MISSING_VARIABLE = 'Missing Variable', + MISSING_FEATURE = 'Missing Feature', + MISSING_VARIATION = 'Missing Variation', + MISSING_VARIABLE_FOR_VARIATION = 'Missing Variable for Variation', + USER_NOT_IN_ROLLOUT = 'User Not in Rollout', + USER_NOT_TARGETED = 'User Not Targeted', + INVALID_VARIABLE_TYPE = 'Invalid Variable Type', + TYPE_MISMATCH = 'Variable Type Mismatch', + UNKNOWN = 'Unknown', + ERROR = 'Error', +} + +export type EvalReason = { + reason: EVAL_REASONS + details?: string +} + const boolTransform = ({ value }: { value: unknown }) => { if (value === 'true') { return true @@ -345,6 +396,7 @@ export type SDKVariable = PublicVariable & { value: VariableValue _feature?: string evalReason?: unknown + eval?: EvalReason } export type SDKFeature = Pick< @@ -354,7 +406,7 @@ export type SDKFeature = Pick< _variation: string variationName: string variationKey: string - evalReason?: unknown + eval?: EvalReason } type FeatureVariation = { diff --git a/lib/shared/types/src/types/config/models/audience.ts b/lib/shared/types/src/types/config/models/audience.ts index 3a054282f..f09435588 100644 --- a/lib/shared/types/src/types/config/models/audience.ts +++ b/lib/shared/types/src/types/config/models/audience.ts @@ -77,7 +77,6 @@ export enum FilterType { export enum UserSubType { user_id = 'user_id', email = 'email', - ip = 'ip', country = 'country', platform = 'platform', platformVersion = 'platformVersion', diff --git a/sdk/js-cloud-server/__tests__/cloudClient.spec.ts b/sdk/js-cloud-server/__tests__/cloudClient.spec.ts index e3c331ca9..638fcdc4c 100644 --- a/sdk/js-cloud-server/__tests__/cloudClient.spec.ts +++ b/sdk/js-cloud-server/__tests__/cloudClient.spec.ts @@ -38,6 +38,9 @@ describe('DevCycleCloudClient without EdgeDB', () => { expect(res.isDefaulted).toBe(false) expect(res.key).not.toContain('edgedb') expect(res.type).toEqual('Boolean') + expect(res.eval).toEqual({ + reason: 'TARGETING_MATCH', + }) await expect( client.variableValue(user, 'test-key', false), @@ -65,6 +68,10 @@ describe('DevCycleCloudClient without EdgeDB', () => { expect(res.value).toBe(false) expect(res.isDefaulted).toBe(true) expect(res.type).toEqual('Boolean') + expect(res.eval).toEqual({ + reason: 'DEFAULT', + details: 'Error', + }) await expect( client.variableValue(user, 'test-key-not-in-config', false), @@ -135,6 +142,9 @@ describe('DevCycleCloudClient without EdgeDB', () => { value: true, type: 'Boolean', defaultValue: false, + eval: { + reason: 'TARGETING_MATCH', + }, }, }) }) @@ -167,6 +177,9 @@ describe('DevCycleCloudClient without EdgeDB', () => { variationName: 'Variation Name', key: 'test-feature', type: 'release', + eval: { + reason: 'TARGETING_MATCH', + }, }, }) }) @@ -259,6 +272,9 @@ describe('DevCycleCloudClient with EdgeDB Enabled', () => { expect(res.key).toContain('test-key') expect(res.value).toBe(true) expect(res.isDefaulted).toBe(false) + expect(res.eval).toEqual({ + reason: 'OPT_IN', + }) await expect( client.variableValue(user, 'test-key', false), @@ -274,6 +290,10 @@ describe('DevCycleCloudClient with EdgeDB Enabled', () => { expect(res.key).not.toContain('edgedb') expect(res.value).toBe(false) expect(res.isDefaulted).toBe(true) + expect(res.eval).toEqual({ + reason: 'DEFAULT', + details: 'Error', + }) await expect( client.variableValue(user, 'test-key-not-in-config', false), @@ -344,6 +364,9 @@ describe('DevCycleCloudClient with EdgeDB Enabled', () => { value: true, type: 'Boolean', defaultValue: false, + eval: { + reason: 'OPT_IN', + }, }, }) }) @@ -376,6 +399,9 @@ describe('DevCycleCloudClient with EdgeDB Enabled', () => { variationName: 'Variation Name', key: 'test-feature-edgedb', type: 'release', + eval: { + reason: 'OPT_IN', + }, }, }) }) diff --git a/sdk/js-cloud-server/__tests__/models/variable.spec.ts b/sdk/js-cloud-server/__tests__/models/variable.spec.ts index 77c0a7abb..3fe97d814 100644 --- a/sdk/js-cloud-server/__tests__/models/variable.spec.ts +++ b/sdk/js-cloud-server/__tests__/models/variable.spec.ts @@ -8,7 +8,9 @@ describe('DVCVariable Unit Tests', () => { defaultValue: false, value: true, type: VariableType.boolean, - evalReason: 'reason', + eval: { + reason: 'reason', + }, }) expect(variable).toEqual({ key: 'key', @@ -16,7 +18,9 @@ describe('DVCVariable Unit Tests', () => { value: true, defaultValue: false, type: 'Boolean', - evalReason: 'reason', + eval: { + reason: 'reason', + }, }) }) diff --git a/sdk/js-cloud-server/src/__mocks__/handlers.ts b/sdk/js-cloud-server/src/__mocks__/handlers.ts index 8f6924948..811fd2f77 100644 --- a/sdk/js-cloud-server/src/__mocks__/handlers.ts +++ b/sdk/js-cloud-server/src/__mocks__/handlers.ts @@ -1,4 +1,5 @@ // src/mocks/handlers.js +import { EVAL_REASONS } from '@devcycle/types' import { http, HttpResponse } from 'msw' export const handlers = [ @@ -28,6 +29,9 @@ export const handlers = [ value: true, type: 'Boolean', defaultValue: false, + eval: { + reason: EVAL_REASONS.OPT_IN, + }, }, { status: 200 }, ) @@ -38,6 +42,9 @@ export const handlers = [ value: true, type: 'Boolean', defaultValue: false, + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + }, }, { status: 200 }, ) @@ -67,6 +74,9 @@ export const handlers = [ value: true, type: 'Boolean', defaultValue: false, + eval: { + reason: EVAL_REASONS.OPT_IN, + }, }, }, { status: 200 }, @@ -79,6 +89,9 @@ export const handlers = [ value: true, type: 'Boolean', defaultValue: false, + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + }, }, }, { status: 200 }, @@ -112,6 +125,9 @@ export const handlers = [ variationName: 'Variation Name', key: 'test-feature-edgedb', type: 'release', + eval: { + reason: EVAL_REASONS.OPT_IN, + }, }, }, { status: 200 }, @@ -126,6 +142,9 @@ export const handlers = [ variationName: 'Variation Name', key: 'test-feature', type: 'release', + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + }, }, }, { status: 200 }, diff --git a/sdk/js-cloud-server/src/cloudClient.ts b/sdk/js-cloud-server/src/cloudClient.ts index 168edb3f0..3c7968de3 100644 --- a/sdk/js-cloud-server/src/cloudClient.ts +++ b/sdk/js-cloud-server/src/cloudClient.ts @@ -12,8 +12,10 @@ import { DevCyclePlatformDetails, } from './models/populatedUser' import { + DEFAULT_REASON_DETAILS, DevCycleServerSDKOptions, DVCLogger, + EVAL_REASONS, getVariableTypeFromValue, InferredVariableType, VariableDefinitions, @@ -130,7 +132,15 @@ export class DevCycleCloudClient< `Type mismatch for variable ${key}. Expected ${type}, got ${variableResponse.type}`, ) // Default Variable - return new DVCVariable({ key, type, defaultValue }) + return new DVCVariable({ + key, + type, + defaultValue, + eval: { + reason: EVAL_REASONS.DEFAULT, + details: DEFAULT_REASON_DETAILS.TYPE_MISMATCH, + }, + }) } return new DVCVariable({ @@ -138,6 +148,7 @@ export class DevCycleCloudClient< type, defaultValue, value: variableResponse.value as VariableTypeAlias, + eval: variableResponse.eval, }) } catch (err) { throwIfUserError(err) @@ -149,7 +160,15 @@ export class DevCycleCloudClient< ) } // Default Variable - return new DVCVariable({ key, type, defaultValue }) + return new DVCVariable({ + key, + type, + defaultValue, + eval: { + reason: EVAL_REASONS.DEFAULT, + details: DEFAULT_REASON_DETAILS.ERROR, + }, + }) } } diff --git a/sdk/js-cloud-server/src/models/variable.ts b/sdk/js-cloud-server/src/models/variable.ts index 09f77dc0f..41876e31c 100644 --- a/sdk/js-cloud-server/src/models/variable.ts +++ b/sdk/js-cloud-server/src/models/variable.ts @@ -17,6 +17,7 @@ export type VariableParam = { value?: VariableTypeAlias type: VariableType evalReason?: unknown + eval?: unknown } export class DVCVariable< @@ -29,10 +30,10 @@ export class DVCVariable< readonly defaultValue: T readonly isDefaulted: boolean readonly type: 'String' | 'Number' | 'Boolean' | 'JSON' - readonly evalReason?: unknown + readonly eval?: unknown constructor(variable: VariableParam) { - const { key, defaultValue, value, evalReason, type } = variable + const { key, defaultValue, value, eval: evalReason, type } = variable checkParamDefined('key', key) checkParamDefined('defaultValue', defaultValue) checkParamType('key', key, typeEnum.string) @@ -45,7 +46,7 @@ export class DVCVariable< ? (defaultValue as unknown as VariableTypeAlias) : value this.defaultValue = defaultValue - this.evalReason = evalReason + this.eval = evalReason this.type = type } } diff --git a/sdk/js-cloud-server/src/types.ts b/sdk/js-cloud-server/src/types.ts index 1be78d524..9509f13b2 100644 --- a/sdk/js-cloud-server/src/types.ts +++ b/sdk/js-cloud-server/src/types.ts @@ -1,11 +1,4 @@ -import { - DVCLogger, - DVCDefaultLogLevel, - DVCReporter, - DVCCustomDataJSON, - VariableValue, - DVCJSON, -} from '@devcycle/types' +import { DVCCustomDataJSON, VariableValue, DVCJSON } from '@devcycle/types' export type DVCVariableValue = VariableValue export type JSON = DVCJSON @@ -48,7 +41,7 @@ export interface DVCVariableInterface { * Evaluation Reason as to why the variable was segmented into a specific Feature and * given this specific value */ - readonly evalReason?: unknown + readonly eval?: unknown } export interface DevCycleEvent { @@ -89,7 +82,7 @@ export interface DVCFeature { readonly type: string - readonly evalReason?: unknown + readonly eval?: unknown } export type DVCFeatureSet = Record diff --git a/sdk/js/__tests__/Variable.spec.ts b/sdk/js/__tests__/Variable.spec.ts index 15588a470..c7cf387cf 100644 --- a/sdk/js/__tests__/Variable.spec.ts +++ b/sdk/js/__tests__/Variable.spec.ts @@ -20,12 +20,12 @@ describe('DVCVariable tests', () => { key: 'variableKey', defaultValue: false, value: 4, - evalReason: {}, + eval: {}, } as any) expect(variable.key).toBe('variablekey') expect(variable.defaultValue).toBe(false) expect(variable.value).toBe(4) - expect(variable.evalReason).toEqual(expect.any(Object)) + expect(variable.eval).toEqual(expect.any(Object)) }) it('should set variable value to false and not default value', () => { diff --git a/sdk/js/src/Client.ts b/sdk/js/src/Client.ts index 5181efb59..14715882f 100644 --- a/sdk/js/src/Client.ts +++ b/sdk/js/src/Client.ts @@ -341,7 +341,7 @@ export class DevCycleClient< if (configVariable) { if (configVariable.type === type) { data.value = configVariable.value as VariableTypeAlias - data.evalReason = configVariable.evalReason + data.eval = configVariable.eval } else { this.logger.warn( `Type mismatch for variable ${key}. Expected ${type}, got ${configVariable.type}`, diff --git a/sdk/js/src/Variable.ts b/sdk/js/src/Variable.ts index a27ef7aa3..55cc37a7c 100644 --- a/sdk/js/src/Variable.ts +++ b/sdk/js/src/Variable.ts @@ -1,12 +1,12 @@ import { DVCVariable as Variable, DVCVariableValue } from './types' import { checkParamDefined, checkParamType } from './utils' -import type { VariableTypeAlias } from '@devcycle/types' +import type { EvalReason, VariableTypeAlias } from '@devcycle/types' export interface DVCVariableOptions { key: string defaultValue: T value?: VariableTypeAlias - evalReason?: any + eval?: EvalReason } export class DVCVariable implements Variable { @@ -16,7 +16,7 @@ export class DVCVariable implements Variable { callback?: (value: VariableTypeAlias) => void readonly defaultValue: T isDefaulted: boolean - readonly evalReason: any + readonly eval?: EvalReason constructor(variable: DVCVariableOptions) { const { key, defaultValue } = variable @@ -33,7 +33,7 @@ export class DVCVariable implements Variable { } this.defaultValue = variable.defaultValue - this.evalReason = variable.evalReason + this.eval = variable.eval } onUpdate(callback: (value: VariableTypeAlias) => void): DVCVariable { diff --git a/sdk/js/src/types.ts b/sdk/js/src/types.ts index 513de0e3b..afe412d7f 100644 --- a/sdk/js/src/types.ts +++ b/sdk/js/src/types.ts @@ -10,6 +10,7 @@ import type { VariableKey, InferredVariableType, SSEConnectionConstructor, + EvalReason, } from '@devcycle/types' export { UserError } from '@devcycle/types' @@ -24,7 +25,7 @@ export interface ErrorCallback { export type DVCVariableSet = { [key: string]: Pick< DVCVariable, - 'key' | 'value' | 'evalReason' + 'key' | 'value' | 'eval' > & { _id: string type: string @@ -38,7 +39,7 @@ export type DVCFeature = { readonly variationName: string readonly key: string readonly type: string - readonly evalReason?: any + readonly eval?: EvalReason } export type DVCFeatureSet = { @@ -229,7 +230,7 @@ export interface DVCVariable< * Evaluation Reason as to why the variable was segmented into a specific Feature and * given this specific value */ - readonly evalReason?: any + readonly eval?: EvalReason /** * Use the onUpdate callback to be notified everytime the value of the variable diff --git a/sdk/nodejs/src/client.ts b/sdk/nodejs/src/client.ts index c3dcade87..abdf83932 100644 --- a/sdk/nodejs/src/client.ts +++ b/sdk/nodejs/src/client.ts @@ -258,7 +258,7 @@ export class DevCycleClient< if (configVariable) { if (type === configVariable.type) { options.value = configVariable.value as VariableTypeAlias - options.evalReason = configVariable.evalReason + options.evalReason = configVariable.eval?.reason } else { this.logger.error( `Type mismatch for variable ${key}. Expected ${type}, got ${configVariable.type}`, diff --git a/sdk/openfeature-angular-provider/__tests__/DevCycleAngularProvider.test.ts b/sdk/openfeature-angular-provider/__tests__/DevCycleAngularProvider.test.ts index 14da8cba9..f60361f82 100644 --- a/sdk/openfeature-angular-provider/__tests__/DevCycleAngularProvider.test.ts +++ b/sdk/openfeature-angular-provider/__tests__/DevCycleAngularProvider.test.ts @@ -1,4 +1,5 @@ global.fetch = jest.fn() +import { EVAL_REASONS } from '@devcycle/types' import DevCycleAngularProvider from '../src/index' import { Client, @@ -42,7 +43,9 @@ async function initOFClient(): Promise<{ value: true, defaultValue: false, isDefaulted: false, - evalReason: undefined, + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + }, onUpdate: jest.fn(), }) } diff --git a/sdk/openfeature-react-provider/__tests__/DevCycleReactProvider.test.ts b/sdk/openfeature-react-provider/__tests__/DevCycleReactProvider.test.ts index b46454211..77229ef25 100644 --- a/sdk/openfeature-react-provider/__tests__/DevCycleReactProvider.test.ts +++ b/sdk/openfeature-react-provider/__tests__/DevCycleReactProvider.test.ts @@ -1,4 +1,5 @@ global.fetch = jest.fn() +import { EVAL_REASONS } from '@devcycle/types' import DevCycleReactProvider from '../src/index' import { Client, @@ -42,7 +43,9 @@ async function initOFClient(): Promise<{ value: true, defaultValue: false, isDefaulted: false, - evalReason: undefined, + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + }, onUpdate: jest.fn(), }) } diff --git a/sdk/openfeature-web-provider/__tests__/DevCycleProvider.test.ts b/sdk/openfeature-web-provider/__tests__/DevCycleProvider.test.ts index 8c905c5cd..49f9d19e7 100644 --- a/sdk/openfeature-web-provider/__tests__/DevCycleProvider.test.ts +++ b/sdk/openfeature-web-provider/__tests__/DevCycleProvider.test.ts @@ -1,4 +1,5 @@ global.fetch = jest.fn() +import { EVAL_REASONS } from '@devcycle/types' import DevCycleProvider from '../src/DevCycleProvider' import { Client, @@ -42,7 +43,9 @@ async function initOFClient(): Promise<{ value: true, defaultValue: false, isDefaulted: false, - evalReason: undefined, + eval: { + reason: EVAL_REASONS.TARGETING_MATCH, + }, onUpdate: jest.fn(), }) } diff --git a/sdk/openfeature-web-provider/src/DevCycleProvider.ts b/sdk/openfeature-web-provider/src/DevCycleProvider.ts index 381bd87ae..12ecd7079 100644 --- a/sdk/openfeature-web-provider/src/DevCycleProvider.ts +++ b/sdk/openfeature-web-provider/src/DevCycleProvider.ts @@ -243,7 +243,8 @@ export default class DevCycleProvider implements Provider { value: variable.value as T, reason: variable.isDefaulted ? StandardResolutionReasons.DEFAULT - : StandardResolutionReasons.TARGETING_MATCH, + : variable.eval?.reason ?? + StandardResolutionReasons.TARGETING_MATCH, } : { value: ofDefaultValue,