-
-
Notifications
You must be signed in to change notification settings - Fork 251
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(suite): distribution for ab testing
- Loading branch information
1 parent
fedf726
commit c16f0f3
Showing
6 changed files
with
264 additions
and
16 deletions.
There are no files selected for viewing
37 changes: 37 additions & 0 deletions
37
packages/suite/src/components/suite/Distribution/DistributionWrapper.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { ReactElement } from 'react'; | ||
|
||
import { useDistribution } from 'src/hooks/distribution/useDistribution'; | ||
|
||
interface DistributionWrapperProps { | ||
idOfDistribution: string; | ||
components: Array<{ | ||
variant: string; | ||
element: ReactElement; | ||
}>; | ||
} | ||
|
||
/** | ||
* @param components last item in components is default | ||
*/ | ||
export const DistributionWrapper = ({ | ||
idOfDistribution, | ||
components, | ||
}: DistributionWrapperProps): ReactElement | null => { | ||
const { distribution, activeDistributionVariant } = useDistribution(idOfDistribution); | ||
const areComponentEmpty = !components.length; | ||
|
||
if (areComponentEmpty) return null; | ||
|
||
const defaultComponent = components[components.length - 1]; | ||
const distributionOrVariantNotFound = !distribution || !activeDistributionVariant; | ||
const distributionAndComponentsMismatch = distribution?.groups.length !== components.length; | ||
|
||
if (distributionOrVariantNotFound || distributionAndComponentsMismatch) | ||
return defaultComponent.element; | ||
|
||
const activeComponent = components.find( | ||
component => component.variant === activeDistributionVariant.variant, | ||
); | ||
|
||
return activeComponent?.element ?? defaultComponent.element; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { useMemo } from 'react'; | ||
|
||
import { selectAnalyticsInstanceId } from '@suite-common/analytics'; | ||
import { selectDistributionById } from '@suite-common/message-system'; | ||
|
||
import { useSelector } from 'src/hooks/suite'; | ||
import { selectActiveDistributionGroup } from 'src/utils/suite/distribution'; | ||
|
||
export const useDistribution = (idOfDistribution: string) => { | ||
const state = useSelector(state => state); | ||
const instanceId = selectAnalyticsInstanceId(state); | ||
const distribution = selectDistributionById(state, idOfDistribution); | ||
const activeDistributionVariant = useMemo( | ||
() => selectActiveDistributionGroup({ instanceId, distribution }), | ||
[instanceId, distribution], | ||
); | ||
|
||
return { | ||
distribution, | ||
activeDistributionVariant, | ||
}; | ||
}; |
23 changes: 23 additions & 0 deletions
23
packages/suite/src/utils/suite/__fixtures__/distribution.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { getWeakRandomId } from '@trezor/utils'; | ||
|
||
// getWeakRandomId is also used for generating instanceId | ||
export const getArrayOfInstanceIds = (count: number) => | ||
Array.from({ length: count }, () => getWeakRandomId(10)); | ||
|
||
export const distributionTest = { | ||
id: 'distribution-test', | ||
priority: 100, | ||
dismissible: false, | ||
variant: 'info' as const, | ||
category: 'distribution' as const, | ||
groups: [ | ||
{ | ||
variant: 'A', | ||
percentage: 20, | ||
}, | ||
{ | ||
variant: 'B', | ||
percentage: 80, | ||
}, | ||
], | ||
}; |
70 changes: 70 additions & 0 deletions
70
packages/suite/src/utils/suite/__tests__/distribution.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { distributionTest, getArrayOfInstanceIds } from 'src/utils/suite/__fixtures__/distribution'; | ||
import { | ||
getDistributionGroupByInclusion, | ||
getInclusionFromInstanceId, | ||
selectActiveDistributionGroup, | ||
} from 'src/utils/suite/distribution'; | ||
|
||
describe('testing distribution utils', () => { | ||
it('test getInclusionFromInstanceId whether returns percentage between 0 and 99', () => { | ||
const arrayOfIds = getArrayOfInstanceIds(100); | ||
const isExistNumberOutOfRange = arrayOfIds.some(id => { | ||
const percentage = getInclusionFromInstanceId(id); | ||
|
||
return percentage < 0 || percentage > 99; | ||
}); | ||
|
||
expect(isExistNumberOutOfRange).toEqual(false); | ||
}); | ||
|
||
it('test getDistributionGroupByInclusion whether instanceId is not in range of variants', () => { | ||
const arrayOfIds = getArrayOfInstanceIds(100); | ||
const isExistInstanceIdNotInVariantRange = arrayOfIds.some(id => { | ||
const inclusion = getInclusionFromInstanceId(id); | ||
const group = getDistributionGroupByInclusion({ | ||
groups: distributionTest.groups, | ||
inclusion, | ||
}); | ||
|
||
return group === undefined; | ||
}); | ||
|
||
expect(isExistInstanceIdNotInVariantRange).toEqual(false); | ||
}); | ||
|
||
it('test selectActiveDistributionGroup share of variant inclusion', () => { | ||
const deviation = 0.05; | ||
const sampleSize = 1000; | ||
let groupACount = 0; | ||
let groupBCount = 0; | ||
|
||
const arrayOfIds = getArrayOfInstanceIds(sampleSize); | ||
|
||
arrayOfIds.forEach(id => { | ||
const selectedGroup = selectActiveDistributionGroup({ | ||
distribution: distributionTest, | ||
instanceId: id, | ||
}); | ||
|
||
if (selectedGroup?.variant === 'A') { | ||
groupACount += 1; | ||
} | ||
|
||
if (selectedGroup?.variant === 'B') { | ||
groupBCount += 1; | ||
} | ||
}); | ||
|
||
const shareA = groupACount / sampleSize; | ||
const shareB = groupBCount / sampleSize; | ||
|
||
expect(shareA).toBeGreaterThanOrEqual( | ||
distributionTest.groups[0].percentage / 100 - deviation, | ||
); | ||
expect(shareA).toBeLessThanOrEqual(distributionTest.groups[0].percentage / 100 + deviation); | ||
expect(shareB).toBeGreaterThanOrEqual( | ||
distributionTest.groups[1].percentage / 100 - deviation, | ||
); | ||
expect(shareB).toBeLessThanOrEqual(distributionTest.groups[1].percentage / 100 + deviation); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { createHash } from 'crypto'; | ||
|
||
import { Groups, Message } from '@suite-common/suite-types'; | ||
import { RequiredKey } from '@trezor/type-utils'; | ||
|
||
interface DistributionMessageProps extends RequiredKey<Message, 'groups'> {} | ||
|
||
interface DistributionCategoriesProps { | ||
distribution: DistributionMessageProps | undefined; | ||
instanceId: string | undefined; | ||
} | ||
|
||
interface DistributionGetGroupByInclusion { | ||
groups: DistributionMessageProps['groups']; | ||
inclusion: number; | ||
} | ||
|
||
/** | ||
* @returns number between 0 and 99 generated from instanceId | ||
*/ | ||
export const getInclusionFromInstanceId = (instanceId: string) => { | ||
const hash = createHash('sha256').update(instanceId).digest('hex').slice(0, 8); | ||
|
||
return parseInt(hash, 16) % 100; | ||
}; | ||
|
||
export const getDistributionGroupByInclusion = ({ | ||
groups, | ||
inclusion, | ||
}: DistributionGetGroupByInclusion): Groups[number] | undefined => { | ||
let currentPercentage = 0; | ||
|
||
const extendedDistribution = groups.map(group => { | ||
const result = { | ||
group, | ||
range: [currentPercentage, currentPercentage + group.percentage - 1], | ||
}; | ||
|
||
currentPercentage += group.percentage; | ||
|
||
return result; | ||
}); | ||
|
||
return extendedDistribution.find( | ||
group => group.range[0] <= inclusion && group.range[1] >= inclusion, | ||
)?.group; | ||
}; | ||
|
||
export const selectActiveDistributionGroup = ({ | ||
distribution, | ||
instanceId, | ||
}: DistributionCategoriesProps): Groups[number] | undefined => { | ||
if (!instanceId || !distribution) return undefined; | ||
|
||
const inclusionFromInstanceId = getInclusionFromInstanceId(instanceId); | ||
const { groups } = distribution; | ||
|
||
const distributionRange = getDistributionGroupByInclusion({ | ||
groups, | ||
inclusion: inclusionFromInstanceId, | ||
}); | ||
|
||
return distributionRange; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters