-
Notifications
You must be signed in to change notification settings - Fork 7
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
ENG-36 Feature flag pattern. See README.md for usage #2648
base: develop
Are you sure you want to change the base?
Conversation
✅ Deploy Preview for decent-interface-dev ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@johnqh please see the file src/constants/common.ts
, there is some additional "feature flag" code implemented there. I think can we probably safely remove all of those, they're probably all on by default at this point. Gotta double check though.
@adamgall Bridged existing functions over so no change is needed for any code using those existing flags. It is also better to extend src/constants/common.ts to add new typed flags in the future while they are using the dependency injection underneath. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is nice. I got a couple or so change requests
} | ||
|
||
get(key: string): any { | ||
return this.urlFlags[key] ?? import.meta.env[key]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like this to instead be return import.meta.env[key] ?? this.urlFlags[key];
so that we can disable a feature on the environment level and not worry about someone setting a url param to get around it, even if it's a very unlikely scenario
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is designed purposely to have url pattern to overwrite build time feature flags. The fundamental question is whether we should allow users to access features hidden behind feature flags. For example, if we deliver code changes with feature flag enabled, and user encounters issues, we can ask him to disable the feature flag to test again, and quickly know if the issue is coded by new code. Let's discussion tomorrow
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally think it would be good to allow both scenarios:
- feature disabled at environment level, enableable at query flag level
- feature enabled at environment level, disableable at query flag level
@@ -1,11 +1,12 @@ | |||
import '@fontsource/space-mono'; | |||
import React from 'react'; | |||
import ReactDOM from 'react-dom/client'; | |||
import { RouterProvider } from 'react-router-dom'; | |||
import { RouterProvider, useSearchParams } from 'react-router-dom'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unused import useSearchParams
} else { | ||
return false; | ||
} | ||
return FeatureFlags.instance?.get(`VITE_APP_FLAG_${feature}`) === 'ON'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we're bridging these two feature flag implementations, can we go all the way? I'm thinking:
- Move all feature-flag code from here to
helpers/featureFlags.ts
(including, IMO,isDevMode
on70
) - In
EnvironmentFeatureFlags
, use theFeatureFlag
type so we're not depending on strings - Update readme to reflect that adding/removing a feature flag should also happen in
const features
on58
- Might as well move
isFeatureEnabled
intoEnvironmentFeatureFlags
. I dunno.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I can do most of these.
Let's discuss the coding style, whether to use "ON" or true/false.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding coding style, i think that since the native format of Environment Variables is string, we should utilize an obvious string ("ON"
), rather than pretend like it's a boolean.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also agree with Kelvin to just consolidate both of these feature flag implementations into one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love dependency injection - though I think current implementation brings mixing conceptually different approaches - and I feel like we should streamline it to either leverage existing reactivity / observability patterns with Zustand & React Router - or we should keep it "vanilla-only" and remove usage of useSearchParams
, moving logic of initializing feature flags from query parameters into constructor of EnvironmentFeatureFlags
@@ -1,11 +1,12 @@ | |||
import '@fontsource/space-mono'; | |||
import React from 'react'; | |||
import ReactDOM from 'react-dom/client'; | |||
import { RouterProvider } from 'react-router-dom'; | |||
import { RouterProvider, useSearchParams } from 'react-router-dom'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like useSearchParams
is not used in this file?
} else { | ||
return false; | ||
} | ||
return FeatureFlags.instance?.get(`VITE_APP_FLAG_${feature}`) === 'ON'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like this === 'ON'
conflicts with documentation - since in the docs we're referring to the value as boolean, while here we still rely on ON
.
I'd like to stick to ON
value - just aesthetic matter, so loosely held preference. But yeah - we need to align implementation
@@ -19,6 +21,11 @@ export default function HomePage() { | |||
} | |||
}, [safe?.address, action]); | |||
|
|||
const [searchParams] = useSearchParams(); | |||
searchParams.forEach((value, key) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably should wrap this forEach
into useEffect
to prevent re-running this loop during every re-rendering. I'm quite sure we wanna run this loop only when searchParams
gets changed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused about why this code lives on the HomePage. What if a user loads a DAO directly and we bypass this HomePage component? Then I think no feature flags are going to be set
static instance?: IFeatureFlags; | ||
} | ||
|
||
export class EnvironmentFeatureFlags implements IFeatureFlags { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if we wanna run this with class implementation in comparison to other patterns we have in the app. I'd prefer using React context or Zustand store to gain better observability and streamline UI update when search parameters are getting changed.
Current isFeatureEnabled
does not correlate with the observability and reactivity that we're gaining through useSearchParams
- so we either should move logic with search params right into this class, obtaining query parameters using vanilla browser API, or refactor class into Zustand store - to keep reactivity consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dependency injection is here so we can swap .env feature flags with remote configs (Firebase or LaunchDarkly) in the future. Let me think about how to do this with existing patterns. I don't want it to observe on params because once a user overwrite the flag once, he should go through the app without the param persists in all URLs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with both of these points:
- would be good to follow existing patterns, using zustand for the "in memory" persistence of a flag makes sense
- being able to swap where feature flags come from is important
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
John could you also implement a small "other side" implementation, so this PR goes end to end? Something small, like an emoji in the footer or something. But importantly, this should utilize your new feature flag system. I'm just curious about seeing how this works in practice.
|
||
https://app.dev.decentdao.org?FEATURE_1=true | ||
|
||
From then, the flag holds the value from the URL param until app is refreshed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So to be clear about what this means
- a feature flag query param being present will set an in-memory variable
- i assume that if the query param is not present, it won't unset or change a previously set in-memory value
- i also assume that if the query param is present, it will set the in-memory value true/false depending on query param value
- as long as accessed via
FeatureFlags.instance?.get('FEATURE_1')
, the in-memory value will always be returned to callers - but nothing is being persisted beyond refreshes
I am making a handful of assumptions here that i'll probably work through as i keep reading the code, but my notes here are that it seems like in-memory layer for feature flags doesn't solve much functional purpose... I'm questioning how one could get into a state where they've removed the query param but haven't triggered a browser page reload.
But this is just my rambling thoughts nothing to action on here.
edit: wait do query params not persist when you're clicking around in an app? i thought they did. maybe they don't, duh.
} else { | ||
return false; | ||
} | ||
return FeatureFlags.instance?.get(`VITE_APP_FLAG_${feature}`) === 'ON'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding coding style, i think that since the native format of Environment Variables is string, we should utilize an obvious string ("ON"
), rather than pretend like it's a boolean.
} else { | ||
return false; | ||
} | ||
return FeatureFlags.instance?.get(`VITE_APP_FLAG_${feature}`) === 'ON'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also agree with Kelvin to just consolidate both of these feature flag implementations into one.
static instance?: IFeatureFlags; | ||
} | ||
|
||
export class EnvironmentFeatureFlags implements IFeatureFlags { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with both of these points:
- would be good to follow existing patterns, using zustand for the "in memory" persistence of a flag makes sense
- being able to swap where feature flags come from is important
} | ||
|
||
get(key: string): any { | ||
return this.urlFlags[key] ?? import.meta.env[key]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally think it would be good to allow both scenarios:
- feature disabled at environment level, enableable at query flag level
- feature enabled at environment level, disableable at query flag level
@@ -19,6 +21,11 @@ export default function HomePage() { | |||
} | |||
}, [safe?.address, action]); | |||
|
|||
const [searchParams] = useSearchParams(); | |||
searchParams.forEach((value, key) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused about why this code lives on the HomePage. What if a user loads a DAO directly and we bypass this HomePage component? Then I think no feature flags are going to be set
See README.md for more details.
It uses an Injection, so if/when we use remote feature flags, we can change the injection in main.tsx without changing any downstream code.