diff --git a/.env.development b/.env.development index fcf7ceaa..f78972a5 100644 --- a/.env.development +++ b/.env.development @@ -68,4 +68,8 @@ NEXT_PUBLIC_URLS_BASE_PATH=/newtab NEXT_PUBLIC_URLS_USE_TRAILING_SLASH=true # Media Endpoint -NEXT_PUBLIC_MEDIA_ENDPOINT=https://dev-tab2017-media.gladly.io \ No newline at end of file +NEXT_PUBLIC_MEDIA_ENDPOINT=https://dev-tab2017-media.gladly.io + +########## Growthbook ########## + +NEXT_PUBLIC_GROWTHBOOK_ENV=dev \ No newline at end of file diff --git a/.env.local.info b/.env.local.info index f0324549..efd9cc4d 100644 --- a/.env.local.info +++ b/.env.local.info @@ -26,3 +26,5 @@ ########################################## # As needed, override .env.development # values here. + +NEXT_PUBLIC_GROWTHBOOK_ENV=local \ No newline at end of file diff --git a/.env.preview.info b/.env.preview.info index 2619be16..442b231a 100644 --- a/.env.preview.info +++ b/.env.preview.info @@ -67,3 +67,6 @@ COOKIE_SECURE_SAME_SITE_NONE=true NEXT_PUBLIC_URLS_BASE_PATH=/newtab NEXT_PUBLIC_URLS_USE_TRAILING_SLASH=true +########## Growthbook ########## + +NEXT_PUBLIC_GROWTHBOOK_ENV=qa \ No newline at end of file diff --git a/.env.production.info b/.env.production.info index bc39310d..d0052d9d 100644 --- a/.env.production.info +++ b/.env.production.info @@ -68,3 +68,7 @@ NEXT_PUBLIC_URLS_BASE_PATH=/newtab NEXT_PUBLIC_URLS_USE_TRAILING_SLASH=true # Media Endpoint NEXT_PUBLIC_MEDIA_ENDPOINT=https://prod-tab2017-media.gladly.io + +########## Growthbook ########## + +NEXT_PUBLIC_GROWTHBOOK_ENV=production \ No newline at end of file diff --git a/package.json b/package.json index 9103a513..5ab5aec2 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "@fontsource/poppins": "^4.5.0", + "@growthbook/growthbook-react": "^0.7.0", "@material-ui/core": "^4.11.2", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.11.3-deprecations.1", diff --git a/src/__mocks__/@growthbook/growthbook-react.js b/src/__mocks__/@growthbook/growthbook-react.js new file mode 100644 index 00000000..0972f661 --- /dev/null +++ b/src/__mocks__/@growthbook/growthbook-react.js @@ -0,0 +1,17 @@ +import features from 'src/features/features.json' + +const { GrowthBook, GrowthBookProvider } = jest.requireActual( + '@growthbook/growthbook-react' +) +const mockGrowthBook = jest.createMockFromModule('@growthbook/growthbook-react') + +mockGrowthBook.useGrowthBook = jest.fn(() => { + const growthbook = new GrowthBook() + growthbook.setFeatures(features) + return growthbook +}) + +mockGrowthBook.GrowthBook = jest.fn(() => new GrowthBook()) +mockGrowthBook.GrowthBookProvider = GrowthBookProvider + +module.exports = mockGrowthBook diff --git a/src/__tests__/pages/_app.moduleLoad.test.js b/src/__tests__/pages/_app.moduleLoad.test.js index 8e2d4044..54137660 100644 --- a/src/__tests__/pages/_app.moduleLoad.test.js +++ b/src/__tests__/pages/_app.moduleLoad.test.js @@ -268,3 +268,19 @@ describe('_app: router overrides', () => { }) }) }) + +describe('_app: GrowthBook', () => { + it('initializes app with Growthbook and calls it with features', () => { + expect.assertions(1) + const { GrowthBook } = require('@growthbook/growthbook-react') + const features = require('src/features/features.json') + const mockGrowthbook = { + setFeatures: jest.fn(), + } + GrowthBook.mockImplementation(() => mockGrowthbook) + + // eslint-disable-next-line no-unused-expressions + require('src/pages/_app').default + expect(mockGrowthbook.setFeatures).toHaveBeenCalledWith(features) + }) +}) diff --git a/src/__tests__/pages/index.test.js b/src/__tests__/pages/index.test.js index 613d4da8..e7b7dda2 100644 --- a/src/__tests__/pages/index.test.js +++ b/src/__tests__/pages/index.test.js @@ -10,6 +10,7 @@ import { showMockAchievements, showBackgroundImages, showDevelopmentOnlyMissionsFeature, + showInternalOnly, } from 'src/utils/featureFlags' import flushAllPromises from 'src/utils/testHelpers/flushAllPromises' import Achievement from 'src/components/Achievement' @@ -31,6 +32,7 @@ import InviteFriendsIconContainer from 'src/components/InviteFriendsIconContaine import SquadCounter from 'src/components/SquadCounter' import UserImpactContainer from 'src/components/UserImpactContainer' import useCustomTheming from 'src/utils/hooks/useCustomTheming' +import { useGrowthBook } from '@growthbook/growthbook-react' jest.mock('uuid') uuid.mockReturnValue('some-uuid') @@ -74,6 +76,7 @@ jest.mock('src/components/SquadCounter') jest.mock('src/components/UserImpactContainer') jest.mock('src/utils/pageWrappers/CustomThemeHOC') jest.mock('src/utils/hooks/useCustomTheming') +jest.mock('@growthbook/growthbook-react') const setUpAds = () => { isClientSide.mockReturnValue(true) @@ -852,4 +855,25 @@ describe('index.js', () => { 'testSetMe' ) }) + + it('calls setFeatures on growthbook with correct values', async () => { + expect.assertions(1) + const IndexPage = require('src/pages/index').default + const mockProps = getMockProps() + const mockGrowthbook = { + feature: jest.fn().mockReturnValue({ value: true }), + setAttributes: jest.fn(), + } + useGrowthBook.mockReturnValue(mockGrowthbook) + mount() + + expect(mockGrowthbook.setAttributes).toHaveBeenCalledWith({ + userId: mockProps.data.user.id, + env: process.env.NEXT_PUBLIC_GROWTHBOOK_ENV, + causeId: mockProps.data.user.cause.causeId, + v4BetaEnabled: true, + joined: mockProps.data.user.joined, + isTabTeamMember: showInternalOnly(mockProps.data.user.email), + }) + }) }) diff --git a/src/features/features.json b/src/features/features.json new file mode 100644 index 00000000..7550df7e --- /dev/null +++ b/src/features/features.json @@ -0,0 +1,14 @@ +{ + "test-feature": { + "defaultValue": false, + "rules": [ + { + "condition": { + "isTabTeamMember": true, + "env": "local" + }, + "force": true + } + ] + } +} \ No newline at end of file diff --git a/src/pages/_app.js b/src/pages/_app.js index e0b8ba76..5cc65326 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -22,6 +22,8 @@ import 'src/utils/styles/globalStyles.css' import '@fontsource/poppins/300.css' import '@fontsource/poppins/400.css' import '@fontsource/poppins/500.css' +import { GrowthBook, GrowthBookProvider } from '@growthbook/growthbook-react' +import features from 'src/features/features.json' initAuth() @@ -83,6 +85,14 @@ if (isClientSide()) { // The MUI theme prior to any user-level customization. const standardTheme = createTheme(defaultTheme) +// Context for designing GrowthBook features: https://docs.google.com/document/d/1ru-oO7-OWVM3ByYZseJBQ-Mu8_1LwcLUMNlHdpQ2JN4/edit#heading=h.5qxtlklvlhrs +// When adding a new experiment, design it so that the weights of the experiment should be constant. +// For example to design an experiment and modify experiment, we can: +// - Run an experiment over x% of traffic (e.g. a 50/50 exp over 10% of traffic) +// - Adjust the percentage of traffic the experiment is run over. This allows us to scale the total amount of traffic going to a particular bucket. +const growthbook = new GrowthBook() +growthbook.setFeatures(features) + const MyApp = (props) => { const { Component, pageProps } = props @@ -121,12 +131,14 @@ const MyApp = (props) => { content="minimum-scale=1, initial-scale=1, width=device-width" /> - - - - - - + + + + + + + + ) } diff --git a/src/pages/index.js b/src/pages/index.js index 49605bff..e81877a5 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -14,6 +14,7 @@ import { withAuthUserTokenSSR, AuthAction, } from 'next-firebase-auth' +import { useGrowthBook } from '@growthbook/growthbook-react' // custom components import Achievement from 'src/components/Achievement' @@ -61,6 +62,7 @@ import { showMockAchievements, showBackgroundImages, showDevelopmentOnlyMissionsFeature, + showInternalOnly, } from 'src/utils/featureFlags' import logger from 'src/utils/logger' import FullPageLoader from 'src/components/FullPageLoader' @@ -272,6 +274,7 @@ const getRelayQuery = async ({ AuthUser }) => { hasViewedIntroFlow tabs vcCurrent + joined cause { causeId individualImpactEnabled @@ -340,10 +343,23 @@ const Index = ({ data: fallbackData }) => { } }, []) const { app, user, userImpact } = data || {} - const { currentMission, email, cause } = user || {} + const { id: userId, currentMission, email, cause, joined } = user || {} const { theme, onboarding, causeId, individualImpactEnabled } = cause || {} const { primaryColor, secondaryColor } = theme || {} + const growthbook = useGrowthBook() + + useEffect(() => { + growthbook.setAttributes({ + userId, + env: process.env.NEXT_PUBLIC_GROWTHBOOK_ENV, + causeId, + v4BetaEnabled: true, + joined, + isTabTeamMember: showInternalOnly(email), + }) + }, [causeId, email, growthbook, joined, userId]) + // Set the theme based on cause. const setTheme = useCustomTheming() useEffect( @@ -548,6 +564,9 @@ const Index = ({ data: fallbackData }) => { variant="h5" className={clsx(classes.userMenuItem)} > + {growthbook.feature('test-feature').value ? ( +

Welcome to our site!

+ ) : null} diff --git a/yarn.lock b/yarn.lock index b7350c7a..782257a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1652,6 +1652,18 @@ stream-events "^1.0.1" xdg-basedir "^4.0.0" +"@growthbook/growthbook-react@^0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@growthbook/growthbook-react/-/growthbook-react-0.7.0.tgz#8a94a47dec60cc5ad25ea78dd97760d8f50cb401" + integrity sha512-3+aBMBO3PTfvxaNNiAE8nwej1fhtWwVuPZIwg2hyM4Jsrq3TjP+UPlbJNtT3VndLv5+cuzfVxMcl0Kr5ORlnww== + dependencies: + "@growthbook/growthbook" "^0.16.0" + +"@growthbook/growthbook@^0.16.0": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@growthbook/growthbook/-/growthbook-0.16.2.tgz#e5e5041992be66c684bc058832d925f5c579a96e" + integrity sha512-mEPqDLDSO6n76gp9TLoyvgjkBaFKrH9BJyQNb0PwF4oXXzjbgG2qsIuNL/DKJvvkYFqOK80mbDR6C5xw9HGHyA== + "@grpc/grpc-js@^1.3.2": version "1.4.1" resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.4.1.tgz#799063a4ff7395987d4fceb2aab133629b003840"