Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IS-322 Setup GrowthBook for FE #1449

Merged
merged 10 commits into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env-example
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ export USERNAME=''
export GITGUARDIAN_API_KEY=""

export REACT_APP_IS_SITE_PRIVATISATION_ACTIVE=false

# GrowthBook
export REACT_APP_GROWTHBOOK_CLIENT_KEY=xyz
seaerchin marked this conversation as resolved.
Show resolved Hide resolved
34 changes: 34 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@emotion/react": "^11.7.1",
"@emotion/styled": "^11.6.0",
"@fontsource/ibm-plex-mono": "^5.0.5",
"@growthbook/growthbook-react": "^0.17.0",
"@hello-pangea/dnd": "^16.3.0",
"@hookform/resolvers": "^2.8.2",
"@opengovsg/design-system-react": "^1.3.0",
Expand Down
31 changes: 22 additions & 9 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import "@fontsource/ibm-plex-mono"
import "inter-ui/inter.css"

import { datadogRum } from "@datadog/browser-rum"
import { GrowthBookProvider } from "@growthbook/growthbook-react"
import { ThemeProvider } from "@opengovsg/design-system-react"
import axios from "axios"
import { useEffect } from "react"
Expand All @@ -15,6 +16,8 @@ import { ServicesProvider } from "contexts/ServicesContext"
// Import route selector
import { RouteSelector } from "routing/RouteSelector"

import { getGrowthBookInstance } from "utils/growthbook"

import theme from "theme"

import { DATADOG_RUM_SETTINGS } from "./constants/datadog"
Expand Down Expand Up @@ -64,22 +67,32 @@ if (REACT_APP_ENV === "staging" || REACT_APP_ENV === "production") {
datadogRum.startSessionReplayRecording()
}

// GrowthBook instance
const growthbook = getGrowthBookInstance(
process.env.REACT_APP_GROWTHBOOK_CLIENT_KEY
)

const App = () => {
useEffect(() => {
localStorage.removeItem(LOCAL_STORAGE_SITE_COLORS)

// Load features from the GrowthBook API and keep them up-to-date
growthbook.loadFeatures({ autoRefresh: true })
}, [])

return (
<Router basename={process.env.PUBLIC_URL}>
<ServicesProvider client={apiClient}>
<QueryClientProvider client={queryClient}>
<LoginProvider>
<ThemeProvider theme={theme}>
<RouteSelector />
</ThemeProvider>
</LoginProvider>
</QueryClientProvider>
</ServicesProvider>
<GrowthBookProvider growthbook={growthbook}>
<ServicesProvider client={apiClient}>
<QueryClientProvider client={queryClient}>
<LoginProvider>
<ThemeProvider theme={theme}>
<RouteSelector />
</ThemeProvider>
</LoginProvider>
</QueryClientProvider>
</ServicesProvider>
</GrowthBookProvider>
</Router>
)
}
Expand Down
10 changes: 10 additions & 0 deletions src/constants/featureFlags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { FeatureFlag, FeatureFlagSupportedTypes } from "types/featureFlags"

// List of all FE feature flags corresponding to GrowthBook
// TODO: Add after setting up on GrowthBook
export const featureFlags: Record<string, FeatureFlag> = {
sampleFeature: {
key: "sampleFeature",
type: FeatureFlagSupportedTypes.string,
},
}
36 changes: 34 additions & 2 deletions src/routing/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Box, Center, Spinner } from "@chakra-ui/react"
import { useGrowthBook } from "@growthbook/growthbook-react"
import axios from "axios"
import _ from "lodash"
import { Redirect, Route, RouteProps } from "react-router-dom"
import { useEffect } from "react"
import { Redirect, Route, RouteProps, useLocation } from "react-router-dom"

import { useLoginContext } from "contexts/LoginContext"

import { getDecodedParams } from "utils/decoding"
import { getSiteNameAttributeFromPath } from "utils/growthbook"

import { GBAttributes } from "types/featureFlags"

// axios settings
axios.defaults.withCredentials = true
Expand All @@ -21,7 +26,34 @@ export const ProtectedRoute = ({
component: WrappedComponent,
...rest
}: RouteProps): JSX.Element => {
const { displayedName, isLoading } = useLoginContext()
const {
displayedName,
isLoading,
userId,
userType,
email,
contactNumber,
} = useLoginContext()
const growthbook = useGrowthBook()
const currPath = useLocation().pathname
const siteNameFromPath = getSiteNameAttributeFromPath(currPath)

useEffect(() => {
if (growthbook) {
const gbAttributes: GBAttributes = {
userId,
userType,
email,
displayedName,
contactNumber,
}
// add siteName if it exists
if (siteNameFromPath && siteNameFromPath !== "") {
gbAttributes.siteName = siteNameFromPath
}
growthbook.setAttributes(gbAttributes)
}
}, [userId, userType, email, displayedName, contactNumber, currPath])

if (isLoading) {
return (
Expand Down
22 changes: 22 additions & 0 deletions src/types/featureFlags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const FeatureFlagSupportedTypes = {
boolean: "boolean",
number: "number",
string: "string",
json: "json",
} as const

export type FeatureFlagTypes = typeof FeatureFlagSupportedTypes[keyof typeof FeatureFlagSupportedTypes]

export type FeatureFlag = {
key: string
type: FeatureFlagTypes
}

export type GBAttributes = {
userId: string
userType: string
email: string
displayedName: string
contactNumber: string
siteName?: string
}
20 changes: 20 additions & 0 deletions src/utils/growthbook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { GrowthBook } from "@growthbook/growthbook-react"

const GROWTHBOOK_API_HOST = "https://cdn.growthbook.io"

export const getGrowthBookInstance = (clientKey: string, isDev = false) => {
return new GrowthBook({
apiHost: GROWTHBOOK_API_HOST,
clientKey,
enableDevMode: isDev, // enable only for local dev
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is never set to true - wdyt about removing this argument from getGrowthBookInstance and setting it based on whether REACT_APP_ENV === LOCAL_DEV

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that but in our local dev, our react env is not LOCAL_DEV as per 1PW

Also the intended use is that if devs need this, they add the true to the constructor in App.jsx and by default it will remain false

Copy link
Contributor

@seaerchin seaerchin Aug 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, my main worry is that this value should be true by default. this will result in q abit of overhead when switching between branches, for example (have to stash, switch, pop etc), which is why i thought it might make sense to dynamically compute this on a per env basis.

the tradeoff we're making here seems to be the time taken to standardise REACT_APP_ENV versus how often this might be swapped to true.

if it makes sense for this to be false by default, feel free to resolve this comment. otherwise, maybe just put a message out in the slack channel to standardise this would be ok toO!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seaerchin This option should definitely be false by default to prevent accidental commits or pushes which may pose a higher risk. The docs state:

To avoid exposing all of your internal feature flags and experiments to users, we recommend setting this to false in production in most cases.

I wanted the act of turning this on to be a conscious choice.

But if you are worried about the manual overhead and prevent a more dynamic setting, we could just do this:


export const getGrowthBookInstance = (clientKey: string) => {
  // disable for staging and production
  const isDev =
    process.env.REACT_APP_ENV !== "staging" &&
    process.env.REACT_APP_ENV !== "production"

  return new GrowthBook({
    apiHost: GROWTHBOOK_API_HOST,
    clientKey,
    enableDevMode: isDev, // enable only for local dev
  })
}

Wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh cos that's the production flag - so imagine if you always want to see the new dev stuff, you'd always need to stash then pop

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more inclined to do what you've laid out above - with the only caveat being that we align localDev cos got (non dev) envs that we might wanna have - eg: uat/vapt

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea exactly, so we need to be firm on the definition and there could be future cases where we introduce new environments. To be on the safe side, the flag needs to be a whitelist (assert if specific environment exists) rather than asserting some environment does not match

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can, sounds good! i'll approve it first - feel free to merge once isDev flag is in

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

})
}

export const getSiteNameAttributeFromPath = (path: string) => {
const pathnames = path.split("/")

if (pathnames.length > 2 && pathnames[1] === "sites") {
return pathnames[2]
}
return ""
}