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-310: Setup GrowthBook for BE #926

Merged
merged 44 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
23b591f
feat: add growthbook as dep
harishv7 Aug 23, 2023
38eb9db
feat: add to env example
harishv7 Aug 23, 2023
883e9ce
feat: add polyfill deps for growthbook
harishv7 Aug 23, 2023
c576c54
feat: add utils
harishv7 Aug 23, 2023
ad4aa4b
feat: setup types for growthbook
harishv7 Aug 23, 2023
a12da4e
feat: add extended type for request handler
harishv7 Aug 23, 2023
e41e10d
feat: add middleware
harishv7 Aug 23, 2023
0c33a47
feat: update middleware
harishv7 Aug 23, 2023
9c0eec2
feat: add growthbook to convict
harishv7 Aug 23, 2023
ecb5f0a
feat: use middleware across routes
harishv7 Aug 23, 2023
ff55152
feat: extract into constant file
harishv7 Aug 23, 2023
28dff95
fix: add GB key to .env.test
harishv7 Aug 28, 2023
c81fb75
feat: enable auto-refresh
harishv7 Aug 28, 2023
e081716
feat: introduce interface for GrowthBook
harishv7 Aug 29, 2023
796bf69
feat: add comment explaining new handler type
harishv7 Aug 29, 2023
cc7d519
fix: make return of custom handler type void
harishv7 Aug 29, 2023
deb16f3
fix: imports
harishv7 Aug 29, 2023
3d8b59c
feat: inject attributes from authN middleware
harishv7 Aug 28, 2023
f7c51c5
feat: inject site-name at authorization
harishv7 Aug 28, 2023
4711c42
feat: add type safety for growthbook property in req
harishv7 Aug 29, 2023
a89b798
fix: minor fixes
harishv7 Aug 29, 2023
f88c6d4
feat: inject attributes from authN middleware
harishv7 Aug 28, 2023
6a3c029
feat: add growthbook as optional property in session context
harishv7 Aug 29, 2023
51542fd
feat: inject growthbook into session
harishv7 Aug 29, 2023
affcd18
feat: add type safety for GB
harishv7 Aug 29, 2023
d1e1bbd
feat: add key for ggs whitelist
harishv7 Aug 29, 2023
fb76910
feat: update method to read from feature flag
harishv7 Aug 29, 2023
94232dc
chore: remove env for ggs whitelist
harishv7 Aug 29, 2023
ab2ecb3
fix: remove unused constant
harishv7 Aug 29, 2023
5b6f56e
feat: enable dev mode only for dev node env
harishv7 Aug 29, 2023
ac64458
feat: remove dependency on ggs env var
harishv7 Aug 29, 2023
6e90e5c
feat: make growthbook optional in Express request
harishv7 Aug 29, 2023
772b36d
feat: add type safety
harishv7 Aug 29, 2023
ce18f1e
feat: fix tests
harishv7 Aug 29, 2023
5427be9
fix: conditional logic
harishv7 Aug 29, 2023
0844ec9
fix: log statement
harishv7 Aug 29, 2023
0ac0de6
fix: remove role
harishv7 Aug 29, 2023
9cccabf
feat: add better logging for debugging
harishv7 Aug 29, 2023
b912b30
Merge pull request #928 from isomerpages/IS-506-migrate-ggs-whitelist…
harishv7 Aug 30, 2023
bb9d1d3
Merge pull request #927 from isomerpages/IS-312-inject-user-data-into…
harishv7 Aug 30, 2023
de3d470
fix: use logger instead of console
harishv7 Aug 30, 2023
2777429
feat: update function signatures
harishv7 Aug 30, 2023
7fdd711
feat: update tests
harishv7 Aug 30, 2023
7ed26e6
feat: refactor flags to use namespaced references
harishv7 Aug 30, 2023
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
5 changes: 4 additions & 1 deletion .env-example
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,7 @@ export NETLIFY_ACCESS_TOKEN=""
export SGID_CLIENT_ID=""
export SGID_CLIENT_SECRET=""
export SGID_PRIVATE_KEY=""
export SGID_REDIRECT_URI=""
export SGID_REDIRECT_URI=""

# GrowthBook
export GROWTHBOOK_CLIENT_KEY=""
harishv7 marked this conversation as resolved.
Show resolved Hide resolved
5 changes: 4 additions & 1 deletion .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,7 @@ export NETLIFY_ACCESS_TOKEN="blahblah"
export SGID_CLIENT_ID="client"
export SGID_CLIENT_SECRET="secret"
export SGID_PRIVATE_KEY="private"
export SGID_REDIRECT_URI="http://localhost:8081/v2/auth/sgid/auth-redirect"
export SGID_REDIRECT_URI="http://localhost:8081/v2/auth/sgid/auth-redirect"

# GrowthBook
export GROWTHBOOK_CLIENT_KEY="some random key"
38 changes: 38 additions & 0 deletions package-lock.json

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

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@aws-sdk/client-dynamodb": "^3.370.0",
"@aws-sdk/client-secrets-manager": "^3.370.0",
"@aws-sdk/lib-dynamodb": "^3.370.0",
"@growthbook/growthbook": "^0.27.0",
"@octokit/rest": "^18.12.0",
"@opengovsg/formsg-sdk": "^0.10.0",
"@opengovsg/sgid-client": "^2.0.0",
Expand All @@ -47,9 +48,11 @@
"convict": "^6.2.4",
"cookie-parser": "~1.4.5",
"cors": "^2.8.5",
"cross-fetch": "^4.0.0",
"crypto-js": "^4.1.1",
"dd-trace": "^4.7.0",
"debug": "~2.6.9",
"eventsource": "^2.0.2",
"exponential-backoff": "^3.1.0",
"express": "~4.17.3",
"express-rate-limit": "^6.7.0",
Expand Down
10 changes: 10 additions & 0 deletions src/classes/UserWithSiteSessionData.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { GrowthBook } from "@growthbook/growthbook"

import { FeatureFlags } from "@root/types/featureFlags"

import UserSessionData, { SessionDataProps } from "./UserSessionData"

export type UserWithSiteSessionDataProps = SessionDataProps & {
siteName: string
growthbook?: GrowthBook
}

/**
Expand All @@ -11,9 +16,14 @@ export type UserWithSiteSessionDataProps = SessionDataProps & {
class UserWithSiteSessionData extends UserSessionData {
readonly siteName: string

readonly growthbook?: GrowthBook<FeatureFlags>

constructor(props: UserWithSiteSessionDataProps) {
super(props)
this.siteName = props.siteName
if (props.growthbook) {
this.growthbook = props.growthbook
}
harishv7 marked this conversation as resolved.
Show resolved Hide resolved
}

getGithubParamsWithSite() {
Expand Down
12 changes: 7 additions & 5 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,13 +405,15 @@ const config = convict({
default: "mock-redirect-uri",
},
},
featureFlags: {
ggsWhitelistedRepos: {
doc: "Comma-separated list of whitelisted repos for local Git service",
env: "WHITELISTED_GIT_SERVICE_REPOS",
format: String,
growthbook: {
clientKey: {
doc: "GrowthBook SDK client key",
env: "GROWTHBOOK_CLIENT_KEY",
format: "required-string",
default: "",
},
},
featureFlags: {
ggsTrackedSites: {
doc: "Comma-separated list of tracked sites for GitHub API hits",
env: "GGS_EXPERIMENTAL_TRACKING_SITES",
Expand Down
3 changes: 3 additions & 0 deletions src/constants/featureFlags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const FEATURE_FLAGS = {
GGS_WHITELISTED_REPOS: "ggs_whitelisted_repos",
} as const
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./constants"
export * from "./pages"
export * from "./versions"
export * from "./featureFlags"
15 changes: 15 additions & 0 deletions src/fixtures/sessionData.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { GrowthBook } from "@growthbook/growthbook"

import GithubSessionData from "@root/classes/GithubSessionData"
import UserSessionData from "@root/classes/UserSessionData"
import UserWithSiteSessionData from "@root/classes/UserWithSiteSessionData"
import { FeatureFlags } from "@root/types/featureFlags"

import {
MOCK_USER_EMAIL_ONE,
Expand All @@ -20,6 +23,7 @@ export const mockEmail = "mockEmail"
export const mockTreeSha = "mockTreeSha"
export const mockCurrentCommitSha = "mockCurrentCommitSha"
export const mockSiteName = "mockSiteName"
export const mockGrowthBook = new GrowthBook<FeatureFlags>()

export const mockGithubState = {
treeSha: mockTreeSha,
Expand All @@ -41,6 +45,17 @@ export const mockUserWithSiteSessionData = new UserWithSiteSessionData({
siteName: mockSiteName,
})

export const mockUserWithSiteSessionDataAndGrowthBook = new UserWithSiteSessionData(
{
githubId: mockGithubId,
accessToken: mockAccessToken,
isomerUserId: mockIsomerUserId,
email: mockEmail,
siteName: mockSiteName,
growthbook: mockGrowthBook,
}
)

export const mockGithubSessionData = new GithubSessionData({
treeSha: mockTreeSha,
currentCommitSha: mockCurrentCommitSha,
Expand Down
20 changes: 18 additions & 2 deletions src/middleware/authentication.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import autoBind from "auto-bind"
import { NextFunction, Request, Response } from "express"
import { NextFunction, Response } from "express"
import { Session } from "express-session"

import UserSessionData from "@root/classes/UserSessionData"
import AuthenticationMiddlewareService from "@root/services/middlewareServices/AuthenticationMiddlewareService"
import { SessionData } from "@root/types/express/session"
import { GrowthBookAttributes } from "@root/types/featureFlags"
import { RequestWithGrowthBook } from "@root/types/request"

interface RequestWithSession extends Request {
interface RequestWithSession extends RequestWithGrowthBook {
session: Session & SessionData
}

Expand All @@ -30,6 +32,7 @@ export class AuthenticationMiddleware {
next: NextFunction
) {
const { cookies, originalUrl: url, session } = req

try {
const {
isomerUserId,
Expand All @@ -46,6 +49,19 @@ export class AuthenticationMiddleware {
...rest,
})
res.locals.userSessionData = userSessionData

// populate growthbook
if (req.growthbook) {
const gbAttributes: GrowthBookAttributes = {
isomerUserId,
email,
}
if (session.userInfo.githubId) {
gbAttributes.githubId = session.userInfo.githubId
}
req.growthbook.setAttributes(gbAttributes)
}

return next()
} catch (err) {
return next(err)
Expand Down
31 changes: 31 additions & 0 deletions src/middleware/featureFlag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import UserWithSiteSessionData from "@root/classes/UserWithSiteSessionData"
import config from "@root/config/config"
import logger from "@root/logger/logger"
import { RequestHandlerWithGrowthbook } from "@root/types"
import { getNewGrowthbookInstance } from "@root/utils/growthbook-utils"

export const featureFlagMiddleware: RequestHandlerWithGrowthbook<
never,
unknown,
unknown,
never,
{ userWithSiteSessionData: UserWithSiteSessionData }
> = async (req, res, next) => {
req.growthbook = getNewGrowthbookInstance(config.get("growthbook.clientKey"))

// Clean up at the end of the request
res.on("close", () => {
if (req.growthbook) req.growthbook.destroy()
})

// Wait for features to load (will be cached in-memory for future requests)
req.growthbook
.loadFeatures({ autoRefresh: true })
.then(() => next())
.catch((e: unknown) => {
logger.error(
`Failed to load features from GrowthBook: ${JSON.stringify(e)}`
)
next()
})
}
19 changes: 19 additions & 0 deletions src/middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ import UserWithSiteSessionData from "@classes/UserWithSiteSessionData"

import CollaboratorsService from "@root/services/identity/CollaboratorsService"
import { RequestHandler } from "@root/types"
import { GrowthBookAttributes } from "@root/types/featureFlags"
import IdentityAuthService from "@services/identity/AuthService"
import IsomerAdminsService from "@services/identity/IsomerAdminsService"
import UsersService from "@services/identity/UsersService"
import AuthenticationMiddlewareService from "@services/middlewareServices/AuthenticationMiddlewareService"
import AuthorizationMiddlewareService from "@services/middlewareServices/AuthorizationMiddlewareService"
import FormsProcessingService from "@services/middlewareServices/FormsProcessingService"

import { featureFlagMiddleware } from "./featureFlag"

const getAuthenticationMiddleware = () => {
const authenticationMiddlewareService = new AuthenticationMiddlewareService()
const authenticationMiddleware = new AuthenticationMiddleware({
Expand Down Expand Up @@ -82,11 +85,26 @@ const attachSiteHandler: RequestHandler<
params: { siteName },
} = req
const { userSessionData } = res.locals

// populate growthbook
if (req.growthbook) {
const { isomerUserId, email, githubId } = userSessionData
const gbAttributes: GrowthBookAttributes = {
isomerUserId,
email,
siteName,
}
if (githubId) gbAttributes.githubId = githubId
req.growthbook.setAttributes(gbAttributes)
}

const userWithSiteSessionData = new UserWithSiteSessionData({
...userSessionData.getGithubParams(),
siteName,
growthbook: req.growthbook, // inject into session
})
res.locals.userWithSiteSessionData = userWithSiteSessionData

return next()
}

Expand All @@ -95,4 +113,5 @@ export {
getAuthorizationMiddleware,
attachFormSGHandler,
attachSiteHandler,
featureFlagMiddleware,
}
Loading