Skip to content

Commit

Permalink
feat: added bucketing key logic to shared-bucketing-as (#940)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsalaber authored Aug 20, 2024
1 parent fe41403 commit 8562127
Show file tree
Hide file tree
Showing 12 changed files with 296 additions and 262 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '../bucketingImportHelper'
import testData from '@devcycle/bucketing-test-data/json-data/testData.json'
const { config, barrenConfig, configWithNullCustomData } = testData
import { configWithBucketingKey } from '../helpers/configWithBucketingKey'

import moment from 'moment'
import * as uuid from 'uuid'
Expand Down Expand Up @@ -1629,3 +1630,193 @@ describe('Client Data', () => {
setClientCustomDataJSON({})
})
})

describe('bucketingKey tests', () => {
afterEach(() => cleanupSDK(sdkKey))

it('buckets a user with user_id if no bucketingKey', () => {
const user = {
user_id: 'test-id',
customData: {
favouriteFood: 'pizza',
},
platformVersion: '1.1.2',
email: '[email protected]',
}
const cWithBucketingKey = configWithBucketingKey('user_id')
initSDK(sdkKey, cWithBucketingKey)

const c = generateBucketedConfig(user)

cleanupSDK(sdkKey)

initSDK(sdkKey, config)
const c2 = generateBucketedConfig(user)

expect(c.features.feature5).not.toBeFalsy()
expect(c.features.feature5.variationKey).toEqual(
c2.features.feature5.variationKey,
)
})

it('buckets a user with custom bucketingKey', () => {
const user = {
country: 'U S AND A',
user_id: 'pass_rollout',
customData: {
favouriteFood: 'pizza',
},
privateCustomData: {
favouriteDrink: 'coffee',
},
platformVersion: '1.1.2',
os: 'Android',
email: '[email protected]',
}
const sameUserDifferentFood = {
...user,
customData: {
favouriteFood: 'pasta',
},
}
const differentUserSameFood = {
...sameUserDifferentFood,
user_id: 'a_different_person',
}
const cWithBucketingKey = configWithBucketingKey('favouriteFood')
initSDK(sdkKey, cWithBucketingKey)

const c = generateBucketedConfig(user)
const cSameUserDifferentFood = generateBucketedConfig(
sameUserDifferentFood,
)
const cDifferentUserSameFood = generateBucketedConfig(
differentUserSameFood,
)

expect(c.features.feature5.variationKey).not.toEqual(
cSameUserDifferentFood.features.feature5.variationKey,
)
expect(cSameUserDifferentFood.features.feature5.variationKey).toEqual(
cDifferentUserSameFood.features.feature5.variationKey,
)
})

it('buckets a user with custom bucketingKey from privateCustomData', () => {
const user = {
country: 'U S AND A',
user_id: 'pass_rollout',
privateCustomData: {
favouriteFood: 'pizza',
},
platformVersion: '1.1.2',
os: 'Android',
email: '[email protected]',
}
const sameUserDifferentFood = {
...user,
privateCustomData: {
favouriteFood: 'pasta',
},
}
const differentUserSameFood = {
...sameUserDifferentFood,
user_id: 'a_different_person',
}
const cWithBucketingKey = configWithBucketingKey('favouriteFood')
initSDK(sdkKey, cWithBucketingKey)

const c = generateBucketedConfig(user)
const cSameUserDifferentFood = generateBucketedConfig(
sameUserDifferentFood,
)
const cDifferentUserSameFood = generateBucketedConfig(
differentUserSameFood,
)

expect(c.features.feature5.variationKey).not.toEqual(
cSameUserDifferentFood.features.feature5.variationKey,
)
expect(cSameUserDifferentFood.features.feature5.variationKey).toEqual(
cDifferentUserSameFood.features.feature5.variationKey,
)
})

it('buckets a user with custom number bucketingKey', () => {
const user = {
country: 'U S AND A',
user_id: 'pass_rollout',
privateCustomData: {
favouriteNumber: 610,
},
platformVersion: '1.1.2',
os: 'Android',
email: '[email protected]',
}
const sameUserDifferentNum = {
...user,
privateCustomData: {
favouriteNumber: 52900,
},
}
const differentUserSameNum = {
...sameUserDifferentNum,
user_id: 'a_different_person',
}
const cWithBucketingKey = configWithBucketingKey('favouriteNumber')
initSDK(sdkKey, cWithBucketingKey)

const c = generateBucketedConfig(user)
const cSameUserDifferentNum =
generateBucketedConfig(sameUserDifferentNum)
const cDifferentUserSameNum =
generateBucketedConfig(differentUserSameNum)

expect(c.features.feature5.variationKey).not.toEqual(
cSameUserDifferentNum.features.feature5.variationKey,
)
expect(cSameUserDifferentNum.features.feature5.variationKey).toEqual(
cDifferentUserSameNum.features.feature5.variationKey,
)
})

it('buckets a user with custom boolean bucketingKey', () => {
const user = {
country: 'U S AND A',
user_id: 'pass_rollout',
privateCustomData: {
signed_up: true,
},
platformVersion: '1.1.2',
os: 'Android',
email: '[email protected]',
}
const sameUserDifferentBool = {
...user,
privateCustomData: {
signed_up: false,
},
}
const differentUserSameBool = {
...sameUserDifferentBool,
user_id: 'a_different_person',
}
const cWithBucketingKey = configWithBucketingKey('signed_up')
initSDK(sdkKey, cWithBucketingKey)

const c = generateBucketedConfig(user)
const cSameUserDifferentBool = generateBucketedConfig(
sameUserDifferentBool,
)
const cDifferentUserSameBool = generateBucketedConfig(
differentUserSameBool,
)

expect(c.features.feature5.variationKey).not.toEqual(
cSameUserDifferentBool.features.feature5.variationKey,
)
expect(cSameUserDifferentBool.features.feature5.variationKey).toEqual(
cDifferentUserSameBool.features.feature5.variationKey,
)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { config } from '@devcycle/bucketing-test-data/json-data/testData.json'

export function configWithBucketingKey(bucketingKey: string): unknown {
return {
...config,
features: config.features.map((feature) => ({
...feature,
configuration: {
...feature.configuration,
targets: feature.configuration.targets.map((target) => ({
...target,
bucketingKey,
})),
},
})),
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import cloneDeep from 'lodash/cloneDeep'
import immutable from 'object-path-immutable'
import { Feature } from '@devcycle/types'

import { configWithBucketingKey } from '../helpers/configWithBucketingKey'

const testConfigBodyV2 = (str: string, utf8: boolean): any => {
if (utf8) {
const buff = Buffer.from(str, 'utf8')
Expand Down Expand Up @@ -40,23 +42,26 @@ const postProcessedConfig = (config: unknown) => {
}

describe.each([true, false])('Config Body V2', (utf8) => {
const testConfig = configWithBucketingKey(
'user_id',
) as typeof testData.config
it('should parse valid JSON into ConfigBody class', () => {
expect(testConfigBodyV2(JSON.stringify(testData.config), utf8)).toEqual(
postProcessedConfig(testData.config),
expect(testConfigBodyV2(JSON.stringify(testConfig), utf8)).toEqual(
postProcessedConfig(testConfig),
)
})

it('should parse if missing optional top level field', () => {
const config = cloneDeep(testData.config)
const config = cloneDeep(testConfig)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
delete config.clientSDKKey
expect(testConfigBodyV2(JSON.stringify(config), utf8)).toEqual(
immutable.del(postProcessedConfig(testData.config), 'clientSDKKey'),
immutable.del(postProcessedConfig(testConfig), 'clientSDKKey'),
)
})
it('should throw if target.rollout is missing type', () => {
const config = cloneDeep(testData.config)
const config = cloneDeep(testConfig)
const target: any = config.features[0].configuration.targets[0]
target.rollout = {
startDate: new Date(),
Expand All @@ -67,7 +72,7 @@ describe.each([true, false])('Config Body V2', (utf8) => {
})

it('should parse target.bucketingKey exists', () => {
const config = cloneDeep(testData.config)
const config = cloneDeep(testConfig)
const target: any = config.features[0].configuration.targets[0]
target.bucketingKey = 'bucketingKey'
expect(testConfigBodyV2(JSON.stringify(config), utf8)).toEqual(
Expand All @@ -76,7 +81,7 @@ describe.each([true, false])('Config Body V2', (utf8) => {
})

it('should parse startsWith/endsWith filter', () => {
const config = cloneDeep(testData.config)
const config = cloneDeep(testConfig)
const filters =
config.features[0].configuration.targets[0]._audience.filters
filters.filters[0] = {
Expand All @@ -91,14 +96,18 @@ describe.each([true, false])('Config Body V2', (utf8) => {
})

it('should handle extended UTF8 characters, from UTF8: ' + utf8, () => {
const testConfig = immutable.set(testData.config, 'project.key', '👍 ö')
expect(testConfigBodyV2(JSON.stringify(testConfig), utf8)).toEqual(
postProcessedConfig(testConfig),
const testInvalidConfig = immutable.set(
testConfig,
'project.key',
'👍 ö',
)
expect(
testConfigBodyV2(JSON.stringify(testInvalidConfig), utf8),
).toEqual(postProcessedConfig(testInvalidConfig))
})

it('should throw if feature.type is missing not a valid type', () => {
const config = cloneDeep(testData.config)
const config = cloneDeep(testConfig)
const feature: any = config.features[0]
feature.type = 'invalid'
expect(() => testConfigBodyV2(JSON.stringify(config), utf8)).toThrow(
Expand All @@ -108,7 +117,7 @@ describe.each([true, false])('Config Body V2', (utf8) => {
})

it('should throw if audience is missing fields for user filter', () => {
const config = cloneDeep(testData.config)
const config = cloneDeep(testConfig)
const filters =
config.features[0].configuration.targets[0]._audience.filters
filters.filters[0] = {
Expand All @@ -121,7 +130,7 @@ describe.each([true, false])('Config Body V2', (utf8) => {
})

it('should not throw if audience is using invalid operator', () => {
const config = cloneDeep(testData.config)
const config = cloneDeep(testConfig)
const filters =
config.features[0].configuration.targets[0]._audience.filters
filters.operator = 'xylophone'
Expand All @@ -131,7 +140,7 @@ describe.each([true, false])('Config Body V2', (utf8) => {
})

it('should throw if audience is missing comparator for user filter', () => {
const config = cloneDeep(testData.config)
const config = cloneDeep(testConfig)
const filters =
config.features[0].configuration.targets[0]._audience.filters
filters.filters[0] = {
Expand All @@ -145,7 +154,7 @@ describe.each([true, false])('Config Body V2', (utf8) => {
})

it('should throw if custom data filter is missing dataKey', () => {
const config = cloneDeep(testData.config)
const config = cloneDeep(testConfig)
const filters =
config.features[0].configuration.targets[0]._audience.filters
filters.filters[0] = {
Expand All @@ -161,7 +170,7 @@ describe.each([true, false])('Config Body V2', (utf8) => {
})

it('should throw if custom data filter is missing dataKeyType', () => {
const config = cloneDeep(testData.config)
const config = cloneDeep(testConfig)
const filters =
config.features[0].configuration.targets[0]._audience.filters
filters.filters[0] = {
Expand All @@ -177,7 +186,7 @@ describe.each([true, false])('Config Body V2', (utf8) => {
})

it('should throw if custom data filter has invalid dataKeyType', () => {
const config = cloneDeep(testData.config)
const config = cloneDeep(testConfig)
const filters =
config.features[0].configuration.targets[0]._audience.filters
filters.filters[0] = {
Expand All @@ -194,7 +203,7 @@ describe.each([true, false])('Config Body V2', (utf8) => {
})

it('should pass if audience is missing fields but type is all', () => {
const config = cloneDeep(testData.config)
const config = cloneDeep(testConfig)
const filters =
config.features[0].configuration.targets[0]._audience.filters
filters.filters[0] = {
Expand All @@ -207,7 +216,7 @@ describe.each([true, false])('Config Body V2', (utf8) => {

it('does not fail on multiple iterations of testConfigBodyClass', () => {
for (let i = 0; i < 1000; i++) {
testConfigBodyV2(JSON.stringify(testData.config), utf8)
testConfigBodyV2(JSON.stringify(testConfig), utf8)
}
})
})
Expand Down
Loading

0 comments on commit 8562127

Please sign in to comment.