mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-20 12:03:11 -04:00
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:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 />
|
||||
|
||||
31
client/src/components/Intro/research-banner.tsx
Normal file
31
client/src/components/Intro/research-banner.tsx
Normal 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? You’ll 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;
|
||||
68
client/src/components/growth-book/growth-book-wrapper.tsx
Normal file
68
client/src/components/growth-book/growth-book-wrapper.tsx
Normal 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);
|
||||
@@ -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
37
package-lock.json
generated
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user