feat(client): reimplement Growthbook (#52316)

This commit is contained in:
Ahmad Abdolsaheb
2023-11-17 19:54:43 +03:00
committed by GitHub
parent 3f49498a6b
commit 0edca42609
5 changed files with 121 additions and 65 deletions
@@ -7,6 +7,7 @@ import { connect } from 'react-redux';
import { goToAnchor } from 'react-scrollable-anchor';
import { bindActionCreators, Dispatch, AnyAction } from 'redux';
import { createSelector } from 'reselect';
import { useFeature } from '@growthbook/growthbook-react';
import BearProgressModal from '../../assets/images/components/bear-progress-modal';
import BearBlockCompletion from '../../assets/images/components/bear-block-completion-modal';
@@ -125,6 +126,8 @@ function DonateModal({
setDonationAttempted(true);
};
useFeature('aa-test-in-component');
useEffect(() => {
if (show) {
void playTone('donation');
+1 -50
View File
@@ -14,34 +14,8 @@ const LearnAlert = ({
isDonating
}: LearnAlertProps): JSX.Element | null => {
const { t } = useTranslation();
const researchRecruitment = useFeature('show-research-recruitment-alert');
const universityCreation = useFeature('university-creation-alert');
const seasonalMessage = useFeature('seasonal-alert');
const researchRecruitmentAlert = (
<Alert variant='info' className='annual-donation-alert'>
<p>
<b>Launching Oct 19</b>: freeCodeCamp is teaming up with researchers
from Stanford and UPenn to study how to help people build strong coding
habits.
</p>
<p style={{ marginBottom: 20, marginTop: 14 }}>
Would you like to get involved? Youll get free coaching from our
scientists.
</p>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<Link
className='btn'
key='donate'
sameTab={false}
to='https://wharton.qualtrics.com/jfe/form/SV_57rJfXROkQDDU2y'
>
Learn about HabitLab
</Link>
</div>
</Alert>
);
const seasonalMessageAlert = (
<Alert variant='info' className='annual-donation-alert'>
<p>
@@ -63,30 +37,7 @@ const LearnAlert = ({
</Alert>
);
const universityCreationAlert = (
<Alert variant='info' className='annual-donation-alert'>
<p>
<b>{t('learn.building-a-university')}</b>
</p>
<p>{t('learn.if-help-university')}</p>
<hr />
<p className={'text-center'}>
<Link
className='btn'
key='donate'
sameTab={false}
to='/donate'
onClick={onDonationAlertClick}
>
{t('donate.become-supporter')}
</Link>
</p>
</Alert>
);
if (researchRecruitment.on) return researchRecruitmentAlert;
if (universityCreation.on && !isDonating) return universityCreationAlert;
if (seasonalMessage.on) return seasonalMessageAlert;
if (seasonalMessage.on && !isDonating) return seasonalMessageAlert;
return null;
};
@@ -1,5 +1,4 @@
import React, { ReactNode, useEffect } from 'react';
import sha1 from 'sha-1';
import {
FeatureDefinition,
GrowthBook,
@@ -7,9 +6,14 @@ import {
} from '@growthbook/growthbook-react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { isSignedInSelector, userSelector } from '../../redux/selectors';
import {
isSignedInSelector,
userSelector,
userFetchStateSelector
} from '../../redux/selectors';
import envData from '../../../config/env.json';
import { User } from '../../redux/prop-types';
import { User, UserFetchState } from '../../redux/prop-types';
import { getUUID } from '../../utils/growthbook-cookie';
import GrowthBookReduxConnector from './growth-book-redux-connector';
const { clientLocale, growthbookUri } = envData as {
@@ -37,9 +41,11 @@ const growthbook = new GrowthBook({
const mapStateToProps = createSelector(
isSignedInSelector,
userSelector,
(isSignedIn, user: User) => ({
userFetchStateSelector,
(isSignedIn, user: User, userFetchState: UserFetchState) => ({
isSignedIn,
user
user,
userFetchState
})
);
@@ -48,10 +54,20 @@ interface GrowthBookWrapper extends StateProps {
children: ReactNode;
}
interface UserAttributes {
id: string;
clientLocal: string;
staff?: boolean;
joinDateUnix?: number;
completedChallengesLength?: number;
signedIn?: true;
}
const GrowthBookWrapper = ({
children,
isSignedIn,
user
user,
userFetchState
}: GrowthBookWrapper) => {
useEffect(() => {
async function setGrowthBookFeatures() {
@@ -73,16 +89,24 @@ const GrowthBookWrapper = ({
}, []);
useEffect(() => {
if (isSignedIn) {
growthbook.setAttributes({
id: sha1(user.email),
staff: user.email.includes('@freecodecamp'),
clientLocal: clientLocale,
joinDateUnix: Date.parse(user.joinDate),
completedChallengesLength: user.completedChallenges.length
});
if (userFetchState.complete) {
let userAttributes: UserAttributes = {
id: getUUID() as string,
clientLocal: clientLocale
};
if (isSignedIn) {
userAttributes = {
...userAttributes,
staff: user.email.includes('@freecodecamp'),
joinDateUnix: Date.parse(user.joinDate),
completedChallengesLength: user.completedChallenges.length,
signedIn: true
};
}
growthbook.setAttributes(userAttributes);
}
}, [isSignedIn, user.email, user.joinDate, user.completedChallenges]);
}, [isSignedIn, user, userFetchState]);
return (
<GrowthBookProvider growthbook={growthbook}>
@@ -0,0 +1,40 @@
import { getUUID } from './growthbook-cookie';
describe('getUUID', () => {
let originalCookie: string;
beforeEach(() => {
global.crypto = {
...global.crypto,
randomUUID: () => '123e4567-e89b-12d3-a456-426614174000'
};
// Save original cookie
originalCookie = document.cookie;
// Clear the cookie before each test
document.cookie.split(';').forEach(c => {
document.cookie = c
.replace(/^ +/, '')
.replace(/=.*/, '=;expires=' + new Date().toUTCString() + ';path=/');
});
});
afterEach(() => {
// Restore original cookie
document.cookie = originalCookie;
});
it('should generate a new UUID if none exists', () => {
const uuid = getUUID();
expect(uuid).toBeDefined();
expect(document.cookie).toContain('gbuuid=' + uuid);
});
it('should return the existing UUID if one exists', () => {
const existingUUID = '123e4567-e89b-12d3-a456-426614174000';
document.cookie = 'gbuuid=' + existingUUID;
const uuid = getUUID();
expect(uuid).toBe(existingUUID);
});
});
+38
View File
@@ -0,0 +1,38 @@
export const getUUID = () => {
const COOKIE_NAME = 'gbuuid';
const COOKIE_DAYS = 400; // 400 days is the max cookie duration for chrome
// use the browsers crypto.randomUUID if set
const genUUID = () => {
if (window?.crypto?.randomUUID) return window.crypto.randomUUID();
// return a random UUID style string`
return ((1e7).toString() + -1e3 + -4e3 + -8e3 + -1e11).replace(
/[018]/g,
c =>
(
Number(c) ^
(crypto.getRandomValues(new Uint8Array(1))[0] &
(15 >> (Number(c) / 4)))
).toString(16)
);
};
const getCookie = (name: string) => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift();
};
const setCookie = (name: string, value: string) => {
const d = new Date();
d.setTime(d.getTime() + 24 * 60 * 60 * 1000 * COOKIE_DAYS);
document.cookie = name + '=' + value + ';path=/;expires=' + d.toUTCString();
};
// get the existing UUID from cookie if set, otherwise create one and store it in the cookie
if (getCookie(COOKIE_NAME)) return getCookie(COOKIE_NAME);
const uuid = genUUID();
setCookie(COOKIE_NAME, uuid);
return uuid;
};