feat: add email to A/B function (#44187)
* feat: add email to A/B function * fix: declare types for sha-1 * Update client/src/utils/A-B-tester.ts Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * feat: add custom dimesions for donation events * feat: re-order if statemetns * Apply suggestions from code review Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * fix: assuage TypeScript * update rename * rename vars * update naming * re add types * Update client/src/redux/ga-saga.js Co-authored-by: Shaun Hamilton <shauhami020@gmail.com> Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
This commit is contained in:
79607
client/package-lock.json
generated
79607
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -119,6 +119,7 @@
|
||||
"rxjs": "6.6.7",
|
||||
"sanitize-html": "2.5.3",
|
||||
"sass.js": "0.11.1",
|
||||
"sha-1": "^1.0.0",
|
||||
"store": "2.0.12",
|
||||
"stream-browserify": "3.0.0",
|
||||
"tone": "14.7.77",
|
||||
|
4
client/src/declarations.d.ts
vendored
4
client/src/declarations.d.ts
vendored
@ -15,6 +15,10 @@ declare module '*.png' {
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module 'sha-1' {
|
||||
export default function sha1(str: string): string;
|
||||
}
|
||||
|
||||
// This has to be declared as var, not let or const, to be added to globalThis
|
||||
// eslint-disable-next-line no-var
|
||||
declare var MathJax: {
|
||||
|
@ -1,9 +1,39 @@
|
||||
import { takeEvery, call, all } from 'redux-saga/effects';
|
||||
/* eslint-disable camelcase */
|
||||
import { takeEvery, call, all, select } from 'redux-saga/effects';
|
||||
import { aBTestConfig } from '../../../config/donation-settings';
|
||||
import ga from '../analytics';
|
||||
import {
|
||||
isSignedInSelector,
|
||||
emailSelector,
|
||||
completionCountSelector,
|
||||
completedChallengesSelector
|
||||
} from '../redux';
|
||||
import { emailToABVariant } from '../utils/A-B-tester';
|
||||
|
||||
const GaTypes = { event: ga.event, page: ga.pageview, modal: ga.modalview };
|
||||
|
||||
function* callGaType({ payload: { type, data } }) {
|
||||
if (
|
||||
type === 'event' &&
|
||||
data.category.includes('Donation') &&
|
||||
aBTestConfig.isTesting
|
||||
) {
|
||||
const isSignedIn = yield select(isSignedInSelector);
|
||||
if (isSignedIn) {
|
||||
const email = yield select(emailSelector);
|
||||
const completedChallengeTotal = yield select(completedChallengesSelector);
|
||||
const completedChallengeSession = yield select(completionCountSelector);
|
||||
const customDimensions = {
|
||||
Test_Variation: emailToABVariant(email).isAVariant ? 'A' : 'B',
|
||||
Test_Type: aBTestConfig.type,
|
||||
Challenges_Completed_Session: completedChallengeSession,
|
||||
Challenges_Completed_Total: completedChallengeTotal.length,
|
||||
URL: window.location.href
|
||||
};
|
||||
data = { ...data, ...customDimensions };
|
||||
}
|
||||
}
|
||||
|
||||
yield call(GaTypes[type], data);
|
||||
}
|
||||
|
||||
|
@ -11,8 +11,8 @@ describe('ga-saga', () => {
|
||||
const mockEventPayload = {
|
||||
type: 'event',
|
||||
data: {
|
||||
category: 'Donation',
|
||||
action: 'year end gift paypal button click'
|
||||
category: 'Map Challenge Click',
|
||||
action: '/learn'
|
||||
}
|
||||
};
|
||||
return (
|
||||
|
@ -201,6 +201,7 @@ export const stepsToClaimSelector = state => {
|
||||
isShowProfile: !user?.profileUI?.isLocked
|
||||
};
|
||||
};
|
||||
export const emailSelector = state => userSelector(state).email;
|
||||
export const isDonatingSelector = state => userSelector(state).isDonating;
|
||||
export const isOnlineSelector = state => state[MainApp].isOnline;
|
||||
export const isServerOnlineSelector = state => state[MainApp].isServerOnline;
|
||||
|
32
client/src/utils/A-B-tester.test.ts
Normal file
32
client/src/utils/A-B-tester.test.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import faker from 'faker';
|
||||
import { emailToABVariant } from './A-B-tester';
|
||||
|
||||
describe('client/src is-email-variation-a', () => {
|
||||
it('Consistently returns the same result for the same input', () => {
|
||||
const preSavedResult = {
|
||||
hash: '23e3cacb302b0c759531faa8b414b23709c26029',
|
||||
isAVariant: true,
|
||||
hashInt: 2
|
||||
};
|
||||
const result = emailToABVariant('foo@freecodecamp.org');
|
||||
expect(result).toEqual(preSavedResult);
|
||||
});
|
||||
it('Distributes A and B variations equaly for 100K random emails', () => {
|
||||
let A = 0;
|
||||
let B = 0;
|
||||
const sampleSize = 100000;
|
||||
faker.seed(123);
|
||||
|
||||
for (let i = 0; i < sampleSize; i++) {
|
||||
if (emailToABVariant(faker.internet.email()).isAVariant) A++;
|
||||
else B++;
|
||||
}
|
||||
|
||||
const isBucketWellDistributed = (variant: number): boolean =>
|
||||
variant > 0.99 * (sampleSize / 2);
|
||||
|
||||
expect(isBucketWellDistributed(A) && isBucketWellDistributed(B)).toEqual(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
21
client/src/utils/A-B-tester.ts
Normal file
21
client/src/utils/A-B-tester.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import sha1 from 'sha-1';
|
||||
|
||||
// This function turns an email to a hash and decides if it should be
|
||||
// an A or B variant for A/B testing
|
||||
|
||||
export function emailToABVariant(email: string): {
|
||||
hash: string;
|
||||
isAVariant: boolean;
|
||||
hashInt: number;
|
||||
} {
|
||||
// turn the email into a number
|
||||
const hash: string = sha1(email);
|
||||
const hashInt = parseInt(hash.slice(0, 1), 16);
|
||||
// turn the number to A or B variant
|
||||
const isAVariant = hashInt % 2 === 0;
|
||||
return {
|
||||
hash,
|
||||
isAVariant,
|
||||
hashInt
|
||||
};
|
||||
}
|
@ -92,6 +92,11 @@ const donationUrls = {
|
||||
|
||||
const patreonDefaultPledgeAmount = 500;
|
||||
|
||||
const aBTestConfig = {
|
||||
isTesting: true,
|
||||
type: 'DistributionTest'
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
durationsConfig,
|
||||
amountsConfig,
|
||||
@ -105,5 +110,6 @@ module.exports = {
|
||||
paypalConfigTypes,
|
||||
paypalConfigurator,
|
||||
donationUrls,
|
||||
patreonDefaultPledgeAmount
|
||||
patreonDefaultPledgeAmount,
|
||||
aBTestConfig
|
||||
};
|
||||
|
8087
package-lock.json
generated
8087
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -102,6 +102,7 @@
|
||||
"@testing-library/jest-dom": "5.15.0",
|
||||
"@testing-library/user-event": "13.5.0",
|
||||
"@types/chai": "4.2.22",
|
||||
"@types/faker": "^5.5.9",
|
||||
"@types/inquirer": "8.1.3",
|
||||
"@types/jest": "27.0.3",
|
||||
"@types/loadable__component": "5.13.4",
|
||||
@ -141,6 +142,7 @@
|
||||
"eslint-plugin-react-hooks": "2.5.1",
|
||||
"eslint-plugin-testing-library": "4.12.4",
|
||||
"execa": "5.1.1",
|
||||
"faker": "^5.5.3",
|
||||
"husky": "7.0.4",
|
||||
"jest": "27.3.1",
|
||||
"js-yaml": "3.14.1",
|
||||
|
Reference in New Issue
Block a user