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

Migrated env feature flags to openfeature #6702

Merged
merged 6 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
31 changes: 31 additions & 0 deletions knowledge_base/ADRs/002-OpenFeature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# ADR 002: OpenFeature as our feature flagging adapter

Using OpenFeature provides use with the flexibility to swap out feature flagging providers.

## Status

Proposed: 240212 by Kurtis Assad (@kurtisassad)

## Context

We need a more robust feature flagging system than what we currently had. We were using .env variables directly.
This was a problem because webpack eagerly evaluates these, which means that if we want to change a feature flag
on production, we would need to perform a re-deploy which would take about 20 minutes. We need a system that would
allow us to toggle these features at runtime.

## Decision

We have decided to use the OpenFeature for our feature flagging system.

## Rationale

OpenFeature allows us to:

1. **Ability to toggle features at runtime**: The main purpose of this system, we want to be able to turn features on or off at runtime
2. **Allow us to roll out these features to a subset of users**: We want the ability to be able to roll out some features to specific communities or users.
3. **Allow us the ability to A/B test features**: We want the ability to A/B test features.

## Consequences

As OpenFeature is an interface across many feature flagging providers, it can impose constraints on some providers,
which would not allow us to utilize their full functionality.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"@babel/register": "^7.4.0",
"@canvas-js/core": "0.5.0-alpha4",
"@istanbuljs/nyc-config-typescript": "^0.1.3",
"@openfeature/web-sdk": "^0.4.12",
kurtisassad marked this conversation as resolved.
Show resolved Hide resolved
"@openzeppelin/contracts": "^2.4.0",
"@openzeppelin/contracts-governance": "npm:@openzeppelin/contracts@^4.3.2",
"@osmonauts/lcd": "^0.10.0",
Expand Down
21 changes: 14 additions & 7 deletions packages/commonwealth/client/scripts/App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { OpenFeatureProvider } from '@openfeature/react-sdk';
import { InMemoryProvider, OpenFeature } from '@openfeature/web-sdk';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import useInitApp from 'hooks/useInitApp';
Expand All @@ -6,6 +8,7 @@ import React, { StrictMode } from 'react';
import { RouterProvider } from 'react-router-dom';
import { ToastContainer } from 'react-toastify';
import { queryClient } from 'state/api/config';
import { featureFlags } from './helpers/feature-flags';
import { CWIcon } from './views/components/component_kit/cw_icons/cw_icon';

const Splash = () => {
Expand All @@ -17,19 +20,23 @@ const Splash = () => {
);
};

OpenFeature.setProvider(new InMemoryProvider(featureFlags));
kurtisassad marked this conversation as resolved.
Show resolved Hide resolved

const App = () => {
const { customDomain, isLoading } = useInitApp();

return (
<StrictMode>
<QueryClientProvider client={queryClient}>
{isLoading ? (
<Splash />
) : (
<RouterProvider router={router(customDomain)} />
)}
<ToastContainer />
<ReactQueryDevtools />
<OpenFeatureProvider client={undefined}>
{isLoading ? (
<Splash />
) : (
<RouterProvider router={router(customDomain)} />
)}
<ToastContainer />
<ReactQueryDevtools />
</OpenFeatureProvider>
</QueryClientProvider>
</StrictMode>
);
Expand Down
29 changes: 21 additions & 8 deletions packages/commonwealth/client/scripts/helpers/feature-flags.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
// As of 240205, we are moving away from the use of env vars for feature flags,
// and towards the use of Unleash for all flag management.
// This is our in memory provider setup. It does not automatically ensure your
// feature flag is set on our Unleash instance (May not be available on prod).
//
// See knowledge_base/Feature-Flags.md for info.
// See knowledge_base/Feature-Flags.md for more info.

const buildFlag = (env: string) => {
return {
variants: {
on: true,
off: false,
},
disabled: false,
defaultVariant: env === 'true' ? 'on' : 'off',
};
};

export const featureFlags = {
proposalTemplates: process.env.FLAG_PROPOSAL_TEMPLATES === 'true',
communityHomepage: process.env.FLAG_COMMUNITY_HOMEPAGE === 'true',
newAdminOnboardingEnabled: process.env.FLAG_NEW_ADMIN_ONBOARDING === 'true',
communityStake: process.env.FLAG_COMMUNITY_STAKE === 'true',
newSignInModal: process.env.FLAG_NEW_SIGN_IN_MODAL === 'true',
proposalTemplates: buildFlag(process.env.FLAG_PROPOSAL_TEMPLATES),
communityHomepage: buildFlag(process.env.FLAG_COMMUNITY_HOMEPAGE),
newAdminOnboarding: buildFlag(process.env.FLAG_NEW_ADMIN_ONBOARDING),
communityStake: buildFlag(process.env.FLAG_COMMUNITY_STAKE),
newSignInModal: buildFlag(process.env.FLAG_NEW_SIGN_IN_MODAL),
};

export type AvailableFeatureFlag = keyof typeof featureFlags;
8 changes: 8 additions & 0 deletions packages/commonwealth/client/scripts/hooks/useFlag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { useBooleanFlagValue } from '@openfeature/react-sdk';
import { AvailableFeatureFlag } from '../helpers/feature-flags';

export const useFlag = (name: AvailableFeatureFlag) => {
return useBooleanFlagValue(name, false, {
updateOnConfigurationChanged: false,
});
};
7 changes: 4 additions & 3 deletions packages/commonwealth/client/scripts/hooks/useWallets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import {
} from '../../../shared/analytics/types';
import NewProfilesController from '../controllers/server/newProfiles';
import { setDarkMode } from '../helpers/darkMode';
import { featureFlags } from '../helpers/feature-flags';
import {
getAddressFromWallet,
loginToAxie,
Expand All @@ -50,6 +49,7 @@ import type {
} from '../views/pages/login/types';
import { useBrowserAnalyticsTrack } from './useBrowserAnalyticsTrack';
import useBrowserWindow from './useBrowserWindow';
import { useFlag } from './useFlag';

type IuseWalletProps = {
initialBody?: LoginActiveStep;
Expand All @@ -62,13 +62,14 @@ type IuseWalletProps = {
};

const useWallets = (walletProps: IuseWalletProps) => {
const newSignInModalEnabled = useFlag('newSignInModal');
const [avatarUrl, setAvatarUrl] = useState<string>();
const [address, setAddress] = useState<string>();
const [activeStep, setActiveStep] = useState<LoginActiveStep>();
const [profiles, setProfiles] = useState<Array<ProfileRowProps>>();
const [sidebarType, setSidebarType] = useState<LoginSidebarType>();
const [username, setUsername] = useState<string>(
featureFlags.newSignInModal ? 'Anonymous' : '',
newSignInModalEnabled ? 'Anonymous' : '',
);
const [email, setEmail] = useState<string>();
const [wallets, setWallets] = useState<Array<IWebWallet<any>>>();
Expand Down Expand Up @@ -382,7 +383,7 @@ const useWallets = (walletProps: IuseWalletProps) => {
setSidebarType('newOrReturning');
setActiveStep('selectAccountType');

if (featureFlags.newSignInModal) {
if (newSignInModalEnabled) {
// Create the account with default values
await onCreateNewAccount(
walletToUse,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { featureFlags } from 'helpers/feature-flags';
import { Navigate } from 'navigation/helpers';
import React, { lazy } from 'react';
import { Route } from 'react-router-dom';
import { withLayout } from 'views/Layout';
import { RouteFeatureFlags } from './Router';

const LandingPage = lazy(() => import('views/pages/landing'));
const WhyCommonwealthPage = lazy(() => import('views/pages/why_commonwealth'));
Expand Down Expand Up @@ -102,7 +102,11 @@ const NewProfilePage = lazy(() => import('views/pages/new_profile'));
const EditNewProfilePage = lazy(() => import('views/pages/edit_new_profile'));
const ProfilePageRedirect = lazy(() => import('views/pages/profile_redirect'));

const CommonDomainRoutes = () => [
const CommonDomainRoutes = ({
proposalTemplatesEnabled,
newAdminOnboardingEnabled,
communityHomepageEnabled,
}: RouteFeatureFlags) => [
<Route
key="/"
path="/"
Expand Down Expand Up @@ -342,7 +346,7 @@ const CommonDomainRoutes = () => [
scoped: true,
})}
/>,
...(featureFlags.communityHomepage
...(communityHomepageEnabled
? [
<Route
key="/:scope/feed"
Expand All @@ -363,7 +367,7 @@ const CommonDomainRoutes = () => [
// DISCUSSIONS END

// CONTRACTS
...(featureFlags.proposalTemplates
...(proposalTemplatesEnabled
? [
<Route
key="/:scope/contracts"
Expand Down Expand Up @@ -412,7 +416,7 @@ const CommonDomainRoutes = () => [
/>,

// ADMIN
...(featureFlags.newAdminOnboardingEnabled
...(newAdminOnboardingEnabled
? [
<Route
key="/:scope/manage/profile"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { featureFlags } from 'helpers/feature-flags';
import { Navigate } from 'navigation/helpers';
import React, { lazy } from 'react';
import { Route } from 'react-router-dom';
import { withLayout } from 'views/Layout';
import { RouteFeatureFlags } from './Router';

const SearchPage = lazy(() => import('views/pages/search'));

Expand Down Expand Up @@ -94,7 +94,10 @@ const NewProfilePage = lazy(() => import('views/pages/new_profile'));
const EditNewProfilePage = lazy(() => import('views/pages/edit_new_profile'));
const ProfilePageRedirect = lazy(() => import('views/pages/profile_redirect'));

const CustomDomainRoutes = () => {
const CustomDomainRoutes = ({
proposalTemplatesEnabled,
newAdminOnboardingEnabled,
}: RouteFeatureFlags) => {
return [
<Route
key="/"
Expand Down Expand Up @@ -279,7 +282,7 @@ const CustomDomainRoutes = () => {
// DISCUSSIONS END

// CONTRACTS
...(featureFlags.proposalTemplates
...(proposalTemplatesEnabled
? [
<Route
key="/contracts"
Expand Down Expand Up @@ -321,7 +324,7 @@ const CustomDomainRoutes = () => {
// CONTRACTS END

// ADMIN
...(featureFlags.newAdminOnboardingEnabled
...(newAdminOnboardingEnabled
? [
<Route
key="/manage/profile"
Expand Down
35 changes: 31 additions & 4 deletions packages/commonwealth/client/scripts/navigation/Router.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { OpenFeature } from '@openfeature/web-sdk';
import CustomDomainRoutes from 'navigation/CustomDomainRoutes';
import React from 'react';
import {
Expand All @@ -10,12 +11,38 @@ import { PageNotFound } from 'views/pages/404';
import CommonDomainRoutes from './CommonDomainRoutes';
import GeneralRoutes from './GeneralRoutes';

const Router = (customDomain: string) =>
createBrowserRouter(
export type RouteFeatureFlags = {
proposalTemplatesEnabled: boolean;
newAdminOnboardingEnabled: boolean;
communityHomepageEnabled: boolean;
};

const Router = (customDomain: string) => {
const client = OpenFeature.getClient();
kurtisassad marked this conversation as resolved.
Show resolved Hide resolved
const proposalTemplatesEnabled = client.getBooleanValue(
'proposalTemplates',
false,
);
const newAdminOnboardingEnabled = client.getBooleanValue(
'newAdminOnboarding',
false,
);
const communityHomepageEnabled = client.getBooleanValue(
'communityHomepageEnabled',
false,
);
const flags = {
proposalTemplatesEnabled,
newAdminOnboardingEnabled,
communityHomepageEnabled,
};

return createBrowserRouter(
createRoutesFromElements([
...GeneralRoutes(),
...(customDomain ? CustomDomainRoutes() : CommonDomainRoutes()),
...(customDomain ? CustomDomainRoutes(flags) : CommonDomainRoutes(flags)),
<Route path="*" element={withLayout(PageNotFound, {})} />,
])
]),
);
};
export default Router;
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { HelpMenuPopover } from 'views/menus/help_menu';
import { AuthModal } from 'views/modals/AuthModal';
import { FeedbackModal } from 'views/modals/feedback_modal';
import { LoginModal } from 'views/modals/login_modal';
import { featureFlags } from '../helpers/feature-flags';
import { useFlag } from '../hooks/useFlag';
import app from '../state';
import { CWDivider } from './components/component_kit/cw_divider';
import { CWIconButton } from './components/component_kit/cw_icon_button';
Expand All @@ -30,6 +30,7 @@ type SublayoutHeaderProps = {
};

export const SublayoutHeader = ({ onMobile }: SublayoutHeaderProps) => {
const newSignInModalEnabled = useFlag('newSignInModal');
const [isFeedbackModalOpen, setIsFeedbackModalOpen] = useState(false);
const navigate = useCommonNavigate();
const {
Expand Down Expand Up @@ -149,7 +150,7 @@ export const SublayoutHeader = ({ onMobile }: SublayoutHeaderProps) => {
onClose={() => setIsFeedbackModalOpen(false)}
open={isFeedbackModalOpen}
/>
{!featureFlags.newSignInModal ? (
{!newSignInModalEnabled ? (
<CWModal
content={
<LoginModal onModalClose={() => setIsAuthModalOpen(false)} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { featureFlags } from 'helpers/feature-flags';
import useUserActiveAccount from 'hooks/useUserActiveAccount';
import { useCommonNavigate } from 'navigation/helpers';
import React, { useState } from 'react';
Expand All @@ -8,6 +7,7 @@ import { useFetchThreadsQuery } from 'state/api/threads';
import { useFetchTopicsQuery } from 'state/api/topics';
import useAdminOnboardingSliderMutationStore from 'state/ui/adminOnboardingCards';
import Permissions from 'utils/Permissions';
import { useFlag } from '../../../hooks/useFlag';
import { CWText } from '../component_kit/cw_text';
import { CWModal } from '../component_kit/new_designs/CWModal';
import { CWButton } from '../component_kit/new_designs/cw_button';
Expand All @@ -16,6 +16,7 @@ import './AdminOnboardingSlider.scss';
import { DismissModal } from './DismissModal';

export const AdminOnboardingSlider = () => {
const newAdminOnboardingEnabled = useFlag('newAdminOnboarding');
useUserActiveAccount();

const community = app.config.chains.getById(app.activeChainId());
Expand Down Expand Up @@ -78,7 +79,7 @@ export const AdminOnboardingSlider = () => {
threads.length > 0 &&
hasAnyIntegration) ||
!(Permissions.isSiteAdmin() || Permissions.isCommunityAdmin()) ||
!featureFlags.newAdminOnboardingEnabled ||
!newAdminOnboardingEnabled ||
[
...shouldHideAdminCardsTemporary,
...shouldHideAdminCardsPermanently,
Expand Down
Loading
Loading