Skip to content

Commit

Permalink
fix(condo): DOMA-9242 fixed feature flags when loading data for the f…
Browse files Browse the repository at this point in the history
…irst time (#4829)

* fix(condo): DOMA-9242 added logs for loading data

* feat(featureflags): DOMA-9242 use "fetch" from "@open-condo/keystone/fetch"

* feat(featureflags): DOMA-9242 fixed import

* feat(featureflags): DOMA-9242 updated deps. Added logic for ready state

* feat(condo): DOMA-9242 use FeaturesReady

* feat(condo): DOMA-9242 updated yarn.lock

* chore(condo): DOMA-9242 try debug

* feat(condo): DOMA-9242 added ssr

* chore(condo): DOMA-9242 try debug

* chore(condo): DOMA-9242 try debug

* fix(condo): DOMA-9242 fixed bug

* chore(condo): DOMA-9242 removed logs

* chore(condo): DOMA-9242 removed unused import
  • Loading branch information
Alllex202 authored Jun 14, 2024
1 parent 9704ad7 commit 0a4cb31
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 79 deletions.
71 changes: 37 additions & 34 deletions apps/condo/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { useRouter } from 'next/router'
import React, { useMemo } from 'react'

import { useDeepCompareEffect } from '@open-condo/codegen/utils/useDeepCompareEffect'
import { FeatureFlagsProvider, useFeatureFlags } from '@open-condo/featureflags/FeatureFlagsContext'
import { FeatureFlagsProvider, useFeatureFlags, FeaturesReady, withFeatureFlags } from '@open-condo/featureflags/FeatureFlagsContext'
import * as AllIcons from '@open-condo/icons'
import { extractReqLocale } from '@open-condo/locales/extractReqLocale'
import { withApollo, WithApolloProps } from '@open-condo/next/apollo'
Expand All @@ -28,6 +28,7 @@ import { hasFeature } from '@condo/domains/common/components/containers/FeatureF
import GlobalStyle from '@condo/domains/common/components/containers/GlobalStyle'
import YandexMetrika from '@condo/domains/common/components/containers/YandexMetrika'
import { LayoutContextProvider } from '@condo/domains/common/components/LayoutContext'
import { Loader } from '@condo/domains/common/components/Loader'
import { MenuItem } from '@condo/domains/common/components/MenuItem'
import PopupSmart from '@condo/domains/common/components/PopupSmart'
import { PostMessageProvider } from '@condo/domains/common/components/PostMessageProvider'
Expand Down Expand Up @@ -446,44 +447,44 @@ const MyApp = ({ Component, pageProps }) => {
</Head>
<ConfigProvider locale={ANT_LOCALES[intl.locale] || ANT_DEFAULT_LOCALE} componentSize='large'>
<CacheProvider value={cache}>
<FeatureFlagsProvider>
<SetupTelegramNotificationsBanner />
<GlobalStyle/>
{shouldDisplayCookieAgreement && <CookieAgreement/>}
<LayoutContextProvider>
<TasksProvider>
<PostMessageProvider>
<TrackingProvider>
<TourProvider>
<SubscriptionProvider>
<GlobalAppsFeaturesProvider>
<GlobalAppsContainer/>
<TicketVisibilityContextProvider>
<ActiveCallContextProvider>
<ConnectedAppsWithIconsContextProvider>
<LayoutComponent menuData={<MenuItems/>} headerAction={HeaderAction}>
<RequiredAccess>
<SetupTelegramNotificationsBanner />
<GlobalStyle/>
{shouldDisplayCookieAgreement && <CookieAgreement/>}
<LayoutContextProvider>
<TasksProvider>
<PostMessageProvider>
<TrackingProvider>
<TourProvider>
<SubscriptionProvider>
<GlobalAppsFeaturesProvider>
<GlobalAppsContainer/>
<TicketVisibilityContextProvider>
<ActiveCallContextProvider>
<ConnectedAppsWithIconsContextProvider>
<LayoutComponent menuData={<MenuItems/>} headerAction={HeaderAction}>
<RequiredAccess>
<FeaturesReady fallback={<Loader fill size='large'/>}>
<Component {...pageProps} />
{
isEndTrialSubscriptionReminderPopupVisible && (
<EndTrialSubscriptionReminderPopup/>
)
}
</RequiredAccess>
</LayoutComponent>
</ConnectedAppsWithIconsContextProvider>
</ActiveCallContextProvider>
</TicketVisibilityContextProvider>
</GlobalAppsFeaturesProvider>
</SubscriptionProvider>
</TourProvider>
</TrackingProvider>
</PostMessageProvider>
</TasksProvider>
</LayoutContextProvider>
<YandexMetrika/>
<PopupSmart />
</FeatureFlagsProvider>
</FeaturesReady>
</RequiredAccess>
</LayoutComponent>
</ConnectedAppsWithIconsContextProvider>
</ActiveCallContextProvider>
</TicketVisibilityContextProvider>
</GlobalAppsFeaturesProvider>
</SubscriptionProvider>
</TourProvider>
</TrackingProvider>
</PostMessageProvider>
</TasksProvider>
</LayoutContextProvider>
<YandexMetrika/>
<PopupSmart />
</CacheProvider>
</ConfigProvider>
<UseDeskWidget/>
Expand Down Expand Up @@ -535,7 +536,9 @@ export default (
ssr: true,
GET_ORGANIZATION_TO_USER_LINK_BY_ID_QUERY: GET_ORGANIZATION_EMPLOYEE_BY_ID_QUERY,
})(
MyApp
withFeatureFlags({ ssr: true })(
MyApp
)
)
)
)
Expand Down
147 changes: 119 additions & 28 deletions packages/featureflags/FeatureFlagsContext.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,35 @@
import { GrowthBook, GrowthBookProvider, useGrowthBook } from '@growthbook/growthbook-react'
import { GrowthBook, GrowthBookProvider, useGrowthBook, FeaturesReady } from '@growthbook/growthbook-react'
import get from 'lodash/get'
import isEmpty from 'lodash/isEmpty'
import isEqual from 'lodash/isEqual'
import { NextPage } from 'next'
import getConfig from 'next/config'
import { createContext, useCallback, useContext, useEffect } from 'react'

import { createContext, useCallback, useContext, useEffect, useRef } from 'react'

import {
DEBUG_RERENDERS,
DEBUG_RERENDERS_BY_WHY_DID_YOU_RENDER,
getContextIndependentWrappedInitialProps,
preventInfinityLoop,
} from '@open-condo/next/_utils'
import { useAuth } from '@open-condo/next/auth'
import { useOrganization } from '@open-condo/next/organization'


const {
publicRuntimeConfig: {
serverUrl,
},
} = getConfig()

const growthbook = new GrowthBook()
const FEATURES_RE_FETCH_INTERVAL = 10 * 1000

type UseFlagValueType = <T>(name: string) => T | null

interface IFeatureFlagsContext {
useFlag: (name: string) => boolean,
useFlagValue: UseFlagValueType,
useFlag: (name: string) => boolean
useFlagValue: UseFlagValueType
updateContext: (context) => void
}

Expand All @@ -26,21 +41,16 @@ const FeatureFlagsContext = createContext<IFeatureFlagsContext>({

const useFeatureFlags = (): IFeatureFlagsContext => useContext(FeatureFlagsContext)

const FeatureFlagsProviderWrapper = ({ children }) => {
const FeatureFlagsProviderWrapper = ({ children, initFeatures = null }) => {
const growthbook = useGrowthBook()
const { user } = useAuth()
const { organization } = useOrganization()
const { user, isLoading: userIsLoading } = useAuth()
const { organization, isLoading: organizationIsLoading } = useOrganization()
const featuresRef = useRef(initFeatures)

const isSupport = get(user, 'isSupport', false)
const isAdmin = get(user, 'isAdmin', false)
const userId = get(user, 'id', null)

const {
publicRuntimeConfig: {
serverUrl,
},
} = getConfig()

const updateContext = useCallback((context) => {
const previousContext = growthbook.getAttributes()

Expand All @@ -51,17 +61,27 @@ const FeatureFlagsProviderWrapper = ({ children }) => {

useEffect(() => {
const fetchFeatures = () => {
if (serverUrl) {
fetch(`${serverUrl}/api/features`)
.then((res) => res.json())
.then((features) => {
const prev = growthbook.getFeatures()
if (!isEqual(prev, features)) {
growthbook.setFeatures(features)
}
})
.catch(e => console.error(e))
}
if (!serverUrl) return

const prev = growthbook.getFeatures()
let next = prev
fetch(`${serverUrl}/api/features`)
.then((res) => res.json())
.then((features) => {
next = features
})
.catch(e => {
if (!growthbook.ready && isEmpty(prev)) {
// NOTE: we need to update features so that growthbook is ready to work
next = prev
}
console.error(e)
})
.finally(() => {
if (!growthbook.ready || !isEqual(prev, next)) {
featuresRef.current = next
}
})
}

fetchFeatures()
Expand All @@ -72,6 +92,12 @@ const FeatureFlagsProviderWrapper = ({ children }) => {
}
}, [growthbook, serverUrl])

useEffect(() => {
if (!featuresRef.current || userIsLoading || organizationIsLoading) return

growthbook.setPayload({ features: featuresRef.current })
}, [featuresRef.current, userIsLoading, organizationIsLoading])

useEffect(() => {
updateContext({ isSupport: isSupport || isAdmin, organization: get(organization, 'id'), userId })
}, [updateContext, isAdmin, isSupport, organization, userId])
Expand All @@ -87,14 +113,79 @@ const FeatureFlagsProviderWrapper = ({ children }) => {
)
}

const FeatureFlagsProvider: React.FC = ({ children }) => {
const FeatureFlagsProvider: React.FC<{ initFeatures? }> = ({ children, initFeatures = null }) => {
return (
<GrowthBookProvider growthbook={growthbook}>
<FeatureFlagsProviderWrapper>
<FeatureFlagsProviderWrapper initFeatures={initFeatures}>
{children}
</FeatureFlagsProviderWrapper>
</GrowthBookProvider>
)
}

export { useFeatureFlags, FeatureFlagsProvider }
// @ts-ignore
if (DEBUG_RERENDERS_BY_WHY_DID_YOU_RENDER) FeatureFlagsProvider.whyDidYouRender = true

const initOnRestore = async (ctx) => {
let features = null
const isOnServerSide = typeof window === 'undefined'

if (isOnServerSide) {
try {
const response = await fetch(`${serverUrl}/api/features`)
features = await response.json()
} catch (error) {
console.error('Error while running `withFeatureFlags`', error)
features = null
}
}

return { features }
}

type WithFeatureFlagsProps = {
ssr?: boolean
}
export type WithFeatureFlags = (props: WithFeatureFlagsProps) => (PageComponent: NextPage<any>) => NextPage<any>

const withFeatureFlags: WithFeatureFlags = ({ ssr = false }) => PageComponent => {
const WithFeatureFlags = ({ features, ...pageProps }) => {
if (DEBUG_RERENDERS) console.log('WithFeatureFlags()', features)

return (
<FeatureFlagsProvider initFeatures={features}>
<PageComponent {...pageProps} />
</FeatureFlagsProvider>
)
}

if (DEBUG_RERENDERS_BY_WHY_DID_YOU_RENDER) WithFeatureFlags.whyDidYouRender = true

// Set the correct displayName in development
if (process.env.NODE_ENV !== 'production') {
const displayName = PageComponent.displayName || PageComponent.name || 'Component'
WithFeatureFlags.displayName = `withFeatureFlags(${displayName})`
}

if (ssr || PageComponent.getInitialProps) {
WithFeatureFlags.getInitialProps = async (ctx) => {
if (DEBUG_RERENDERS) console.log('WithIntl.getInitialProps()', ctx)
const isOnServerSide = typeof window === 'undefined'
const { features } = await initOnRestore(ctx)
const pageProps = await getContextIndependentWrappedInitialProps(PageComponent, ctx)

if (isOnServerSide) {
preventInfinityLoop(ctx)
}

return {
...pageProps,
features,
}
}
}

return WithFeatureFlags
}

export { useFeatureFlags, FeatureFlagsProvider, FeaturesReady, withFeatureFlags }
2 changes: 1 addition & 1 deletion packages/featureflags/featureToggleManager.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const { GrowthBook } = require('@growthbook/growthbook')
const { get } = require('lodash')
const fetch = require('node-fetch')

const conf = require('@open-condo/config')
const { fetch } = require('@open-condo/keystone/fetch')
const { getLogger } = require('@open-condo/keystone/logging')
const { getRedisClient } = require('@open-condo/keystone/redis')
const { getFeatureFlag } = require('@open-condo/keystone/test.utils')
Expand Down
8 changes: 4 additions & 4 deletions packages/featureflags/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
"name": "@open-condo/featureflags",
"version": "1.0.0",
"dependencies": {
"@growthbook/growthbook": "^0.18.1",
"@growthbook/growthbook-react": "^0.9.1",
"@growthbook/growthbook": "^1.0.1",
"@growthbook/growthbook-react": "^1.0.1",
"@open-condo/config": "workspace:^",
"@open-condo/keystone": "workspace:^",
"express": "^4.17.1",
"node-fetch": "^2.6.7"
"@open-condo/next": "workspace:^",
"express": "^4.17.1"
},
"peerDependencies": {
"next": ">=9.5.5",
Expand Down
33 changes: 21 additions & 12 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10288,21 +10288,23 @@ __metadata:
languageName: node
linkType: hard

"@growthbook/growthbook-react@npm:^0.9.1":
version: 0.9.1
resolution: "@growthbook/growthbook-react@npm:0.9.1"
"@growthbook/growthbook-react@npm:^1.0.1":
version: 1.0.1
resolution: "@growthbook/growthbook-react@npm:1.0.1"
dependencies:
"@growthbook/growthbook": ^0.18.1
"@growthbook/growthbook": ^1.0.1
peerDependencies:
react: ^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0
checksum: 587b0536d9f0f9180798ff7221a4fed61696e77a3d3e5a782afd46cf0ac01f5519145985fe7bf62ab602eea9f899c1165af885ddabb0f7a8a3e48ac3d4abd6b3
checksum: d13182390f1561e0f464f25205d08ab4e872a29c070bf5541fdab0383e294dd1f62996fa414356cdf0317e4a72415817b203d805b7ea1ef4c2ba7d880ff0edba
languageName: node
linkType: hard

"@growthbook/growthbook@npm:^0.18.1":
version: 0.18.1
resolution: "@growthbook/growthbook@npm:0.18.1"
checksum: 202347550562487b47044f406415f26976ec1fabd0504658f84c6520d834ec8addf57be1ba5e73e4715d02da4c11f41649edbb6ad49b5d2b24ec46a497c5617f
"@growthbook/growthbook@npm:^1.0.1":
version: 1.0.1
resolution: "@growthbook/growthbook@npm:1.0.1"
dependencies:
dom-mutator: ^0.6.0
checksum: 385a6c0a480e1a217cc8000147637eacde1ec04c20b424fb6ac59b1102b28ee5be89e86b2bff4227cfc46993b1870a654cd1216fc4833954135be14f8e6fd1a4
languageName: node
linkType: hard

Expand Down Expand Up @@ -12410,12 +12412,12 @@ __metadata:
version: 0.0.0-use.local
resolution: "@open-condo/featureflags@workspace:packages/featureflags"
dependencies:
"@growthbook/growthbook": ^0.18.1
"@growthbook/growthbook-react": ^0.9.1
"@growthbook/growthbook": ^1.0.1
"@growthbook/growthbook-react": ^1.0.1
"@open-condo/config": "workspace:^"
"@open-condo/keystone": "workspace:^"
"@open-condo/next": "workspace:^"
express: ^4.17.1
node-fetch: ^2.6.7
peerDependencies:
next: ">=9.5.5"
react: ">=16.13.1"
Expand Down Expand Up @@ -25253,6 +25255,13 @@ __metadata:
languageName: node
linkType: hard

"dom-mutator@npm:^0.6.0":
version: 0.6.0
resolution: "dom-mutator@npm:0.6.0"
checksum: f6b32500d9d71f379940022057434db38fd404036968fdbf0d547b31a819c0ad2160edbf67f72e00b8ec7787b5ca04f19d5082fbf33f3644642b16f21e04cbf1
languageName: node
linkType: hard

"dom-serializer@npm:0, dom-serializer@npm:^0.2.1":
version: 0.2.2
resolution: "dom-serializer@npm:0.2.2"
Expand Down

0 comments on commit 0a4cb31

Please sign in to comment.