diff --git a/client/gatsby-browser.js b/client/gatsby-browser.js
index 11616f7cd30..513cc8d8a55 100644
--- a/client/gatsby-browser.js
+++ b/client/gatsby-browser.js
@@ -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 (
- element} />
+
+ element} />
+
);
diff --git a/client/gatsby-ssr.js b/client/gatsby-ssr.js
index c0c0fd3ccaf..e7995a365d0 100644
--- a/client/gatsby-ssr.js
+++ b/client/gatsby-ssr.js
@@ -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 (
- {element}
+
+ {element}
+
);
};
diff --git a/client/package.json b/client/package.json
index 0d399fd50ea..b3099fdfd66 100644
--- a/client/package.json
+++ b/client/package.json
@@ -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",
diff --git a/client/src/components/Intro/index.tsx b/client/src/components/Intro/index.tsx
index f4bad7a7851..22e7bad8425 100644
--- a/client/src/components/Intro/index.tsx
+++ b/client/src/components/Intro/index.tsx
@@ -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 = ({
+
{completedChallengeCount && slug && completedChallengeCount < 15 ? (
diff --git a/client/src/components/Intro/research-banner.tsx b/client/src/components/Intro/research-banner.tsx
new file mode 100644
index 00000000000..324b146bb4b
--- /dev/null
+++ b/client/src/components/Intro/research-banner.tsx
@@ -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 ? (
+
+
+ Launching Oct 19: freeCodeCamp is teaming up with researchers
+ from Stanford and UPenn to study how to help people build strong coding
+ habits.
+
+
+ Would you like to get involved? You’ll get free coaching from our
+ scientists.
+
+
+
+
+
+ ) : null;
+};
+
+ResearchBannerx.displayName = 'ResearchBannerx';
+
+export default ResearchBannerx;
diff --git a/client/src/components/growth-book/growth-book-wrapper.tsx b/client/src/components/growth-book/growth-book-wrapper.tsx
new file mode 100644
index 00000000000..3da37b977b1
--- /dev/null
+++ b/client/src/components/growth-book/growth-book-wrapper.tsx
@@ -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
;
+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;
+ };
+ 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 (
+ {children}
+ );
+};
+
+export default connect(mapStateToProps)(GrowthBookWrapper);
diff --git a/config/read-env.js b/config/read-env.js
index 7dd78f9a90b..50d07fbb1e5 100644
--- a/config/read-env.js
+++ b/config/read-env.js
@@ -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
});
diff --git a/package-lock.json b/package-lock.json
index 9d3be1a5717..56af4535a09 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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"
},
diff --git a/sample.env b/sample.env
index 796deac589c..202fe68220a 100644
--- a/sample.env
+++ b/sample.env
@@ -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
diff --git a/tools/scripts/build/ensure-env.ts b/tools/scripts/build/ensure-env.ts
index 301565a3580..1f296823581 100644
--- a/tools/scripts/build/ensure-env.ts
+++ b/tools/scripts/build/ensure-env.ts
@@ -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);
if (expectedVariables.length !== actualVariables.length) {