feat(client): add growthbook (#48003)

* feat: initial set up

* feat: useFeature setup

* feat: adjust attributes

* chore(client): remove ts-disables in growth-book-wrapper

* feat: pull growthbook uri from env

* feat: adjust the staff atribute

* feat: make linter happy

* feat: update recruitment message

* refactor: simplify types

* chore: delete unused config

* fix: update copy

* fix: add growthbookUri to expected env vars

Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Ahmad Abdolsaheb
2022-10-17 15:53:25 +03:00
committed by GitHub
parent 060826f70b
commit 0df7ee430d
10 changed files with 160 additions and 5 deletions

View File

@@ -8,6 +8,7 @@ import i18n from './i18n/config';
import AppMountNotifier from './src/components/app-mount-notifier';
import { createStore } from './src/redux/createStore';
import layoutSelector from './utils/gatsby/layout-selector';
import GrowthBookProvider from './src/components/growth-book/growth-book-wrapper';
const store = createStore();
@@ -15,7 +16,9 @@ export const wrapRootElement = ({ element }) => {
return (
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<AppMountNotifier render={() => element} />
<GrowthBookProvider>
<AppMountNotifier render={() => element} />
</GrowthBookProvider>
</I18nextProvider>
</Provider>
);

View File

@@ -7,13 +7,16 @@ import i18n from './i18n/config';
import { createStore } from './src/redux/createStore';
import layoutSelector from './utils/gatsby/layout-selector';
import { getheadTagComponents, getPostBodyComponents } from './utils/tags';
import GrowthBookProvider from './src/components/growth-book/growth-book-wrapper';
const store = createStore();
export const wrapRootElement = ({ element }) => {
return (
<Provider store={store}>
<I18nextProvider i18n={i18n}>{element}</I18nextProvider>
<I18nextProvider i18n={i18n}>
<GrowthBookProvider>{element}</GrowthBookProvider>
</I18nextProvider>
</Provider>
);
};

View File

@@ -48,6 +48,7 @@
"@freecodecamp/react-bootstrap": "0.32.3",
"@freecodecamp/react-calendar-heatmap": "1.0.0",
"@freecodecamp/strip-comments": "3.0.1",
"@growthbook/growthbook-react": "0.9.1",
"@loadable/component": "5.15.2",
"@reach/router": "1.3.4",
"@sentry/gatsby": "6.19.7",

View File

@@ -6,6 +6,7 @@ import { Link, Spacer, Loader } from '../helpers';
import IntroDescription from './components/IntroDescription';
import './intro.css';
import ResearchBannerx from './research-banner';
interface IntroProps {
complete?: boolean;
@@ -55,6 +56,7 @@ const Intro = ({
</span>
</blockquote>
</div>
<ResearchBannerx />
{completedChallengeCount && slug && completedChallengeCount < 15 ? (
<div className='intro-description'>
<Spacer />

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { Alert, Button } from '@freecodecamp/react-bootstrap';
import { useFeature } from '@growthbook/growthbook-react';
const ResearchBannerx = (): JSX.Element | null => {
const feature = useFeature('show-research-recruitment-alert');
return feature.on ? (
<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' }}>
<Button
href={'https://wharton.qualtrics.com/jfe/form/SV_57rJfXROkQDDU2y'}
>
Learn about HabitLab
</Button>
</div>
</Alert>
) : null;
};
ResearchBannerx.displayName = 'ResearchBannerx';
export default ResearchBannerx;

View File

@@ -0,0 +1,68 @@
import React, { ReactNode, useEffect } from 'react';
import sha1 from 'sha-1';
import {
FeatureDefinition,
GrowthBook,
GrowthBookProvider
} from '@growthbook/growthbook-react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { isSignedInSelector, userSelector } from '../../redux/selectors';
import envData from '../../../../config/env.json';
import { User } from '../../redux/prop-types';
const { clientLocale, growthbookUri } = envData as {
clientLocale: string;
growthbookUri: string | null;
};
const growthbook = new GrowthBook();
const mapStateToProps = createSelector(
isSignedInSelector,
userSelector,
(isSignedIn, user: User) => ({
isSignedIn,
user
})
);
type StateProps = ReturnType<typeof mapStateToProps>;
interface GrowthBookWrapper extends StateProps {
children: ReactNode;
}
const GrowthBookWrapper = ({
children,
isSignedIn,
user
}: GrowthBookWrapper) => {
if (growthbookUri) {
void (async () => {
const res = await fetch(growthbookUri);
const data = (await res.json()) as {
features: Record<string, FeatureDefinition>;
};
growthbook.setFeatures(data.features);
})();
}
useEffect(() => {
if (isSignedIn) {
const { joinDate, completedChallenges } = user;
growthbook.setAttributes({
id: sha1(user.email),
staff: user.email.includes('@freecodecamp'),
clientLocal: clientLocale,
joinDateUnix: Date.parse(joinDate),
completedChallengesLength: completedChallenges.length
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSignedIn]);
return (
<GrowthBookProvider growthbook={growthbook}>{children}</GrowthBookProvider>
);
};
export default connect(mapStateToProps)(GrowthBookWrapper);

View File

@@ -33,7 +33,8 @@ const {
DEPLOYMENT_ENV: deploymentEnv,
SENTRY_CLIENT_DSN: sentryClientDSN,
SHOW_UPCOMING_CHANGES: showUpcomingChanges,
SHOW_NEW_CURRICULUM: showNewCurriculum
SHOW_NEW_CURRICULUM: showNewCurriculum,
GROWTHBOOK_URI: growthbookUri
} = process.env;
const locations = {
@@ -77,5 +78,9 @@ module.exports = Object.assign(locations, {
? null
: sentryClientDSN,
showUpcomingChanges: showUpcomingChanges === 'true',
showNewCurriculum: showNewCurriculum === 'true'
showNewCurriculum: showNewCurriculum === 'true',
growthbookUri:
!growthbookUri || growthbookUri === 'api_URI_from_Growthbook_dashboard'
? null
: growthbookUri
});

37
package-lock.json generated
View File

@@ -444,6 +444,7 @@
"@freecodecamp/react-bootstrap": "0.32.3",
"@freecodecamp/react-calendar-heatmap": "1.0.0",
"@freecodecamp/strip-comments": "3.0.1",
"@growthbook/growthbook-react": "0.9.1",
"@loadable/component": "5.15.2",
"@reach/router": "1.3.4",
"@sentry/gatsby": "6.19.7",
@@ -3799,6 +3800,28 @@
"version": "2.2.0",
"license": "0BSD"
},
"node_modules/@growthbook/growthbook": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@growthbook/growthbook/-/growthbook-0.18.1.tgz",
"integrity": "sha512-hNNh515lleAUzTch+ezYYVHBteYTIAF1Xxh6+dLJk+BjbhPXUo6X9qNcryIN1b/IMLVnO1ZfE5zbAzVGEQai2w==",
"engines": {
"node": ">=10"
}
},
"node_modules/@growthbook/growthbook-react": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@growthbook/growthbook-react/-/growthbook-react-0.9.1.tgz",
"integrity": "sha512-b4yaMkcIeYQ+j5wND+wsGHbeV33QJoG6vQc4r/7qTDKmq3QhAyMNuLvfy/bYQ/nWnh5TmZArzQ6IRUGTWKfOGg==",
"dependencies": {
"@growthbook/growthbook": "^0.18.1"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": "^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0"
}
},
"node_modules/@hapi/address": {
"version": "2.1.4",
"license": "BSD-3-Clause"
@@ -56971,6 +56994,7 @@
"@freecodecamp/react-bootstrap": "0.32.3",
"@freecodecamp/react-calendar-heatmap": "1.0.0",
"@freecodecamp/strip-comments": "3.0.1",
"@growthbook/growthbook-react": "*",
"@loadable/component": "5.15.2",
"@reach/router": "1.3.4",
"@sentry/gatsby": "6.19.7",
@@ -57577,6 +57601,19 @@
}
}
},
"@growthbook/growthbook": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@growthbook/growthbook/-/growthbook-0.18.1.tgz",
"integrity": "sha512-hNNh515lleAUzTch+ezYYVHBteYTIAF1Xxh6+dLJk+BjbhPXUo6X9qNcryIN1b/IMLVnO1ZfE5zbAzVGEQai2w=="
},
"@growthbook/growthbook-react": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@growthbook/growthbook-react/-/growthbook-react-0.9.1.tgz",
"integrity": "sha512-b4yaMkcIeYQ+j5wND+wsGHbeV33QJoG6vQc4r/7qTDKmq3QhAyMNuLvfy/bYQ/nWnh5TmZArzQ6IRUGTWKfOGg==",
"requires": {
"@growthbook/growthbook": "^0.18.1"
}
},
"@hapi/address": {
"version": "2.1.4"
},

View File

@@ -78,3 +78,6 @@ CODESEE=false
# Webhook proxy url from smee.io for PayPal
WEBHOOK_PROXY_URL=
# Analytics
GROWTHBOOK_URI=api_URI_from_Growthbook_dashboard

View File

@@ -52,12 +52,14 @@ if (FREECODECAMP_NODE_ENV !== 'development') {
const searchKeys = ['algoliaAppId', 'algoliaAPIKey'];
const donationKeys = ['stripePublicKey', 'paypalClientId', 'patreonClientId'];
const loggingKeys = ['sentryClientDSN'];
const abTestingKeys = ['growthbookUri'];
const expectedVariables = locationKeys.concat(
deploymentKeys,
searchKeys,
donationKeys,
loggingKeys
loggingKeys,
abTestingKeys
);
const actualVariables = Object.keys(env as Record<string, unknown>);
if (expectedVariables.length !== actualVariables.length) {