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

[Issue #3249] Feature flag manager refactor #3374

Merged
merged 15 commits into from
Jan 6, 2025

Conversation

doug-s-nava
Copy link
Collaborator

@doug-s-nava doug-s-nava commented Jan 2, 2025

Summary

Fixes #3249

TODOS

  • docs

Time to review: 45 mins

Changes proposed

The existing feature flag system in NextJs consists of a withFeatureFlag wrapper that is used only on the server, a useFeatureFlag hook that is used only on the client, and a FeatureFlagManager that is by both of these implementations and within NextJs middleware. The FeatureFlagManager is able to read environment variables for feature flag values, but as currently written, is unable to make these values available to client side implementations of the FeatureFlagManager.

This change rewrites the FeatureFlagManager and useFeatureFlag hook to solve this issue, primarily making everything rely on all feature flag state being maintained in cookies to allow client and server to remain in sync.

It also:

  • adds the authOff feature flag in frontend code and terraform
  • refactors the environments setup a bit to more easily expose feature flags
  • splits functionality that does not benefit from being held in the FeatureFlagsManager class into a helper file
  • moves feature flag manager file into a nested directory
  • creates a placeholder "auth enabled" text in the header for testing - to be replaced with implementation from [Issue #2653] Sign in component #3281 once that is merged

Context for reviewers

For whoever is reviewing this, I'd appreciate an in person meeting to explain a few things and gather thoughts around the approach.

Test Steps

  1. start server on this branch with npm run build && npm start
  2. visit http://localhost:3000
  3. VERIFY: a sign in button appears in the header
  4. visit http://localhost:3000/opportunity/33?_ff=opportunityOff:false
  5. VERIFY: opportunity page loads
  6. start a server on this branch with npm run build && FEATURE_AUTH_OFF="true" FEATURE_OPPORTUNITY_OFF="true" npm start
  7. visit http://localhost:3000
  8. VERIFY: no sign in button appears
  9. visit http://localhost:3000/opportunity/33?_ff=opportunityOff:false
  10. VERIFY: opportunity page redirects to maintenance
  11. visit http://localhost:3000?_ff=authOff:false
  12. VERIFY: sign in button appears in the header
  13. visit http://localhost:3000/opportunity/33?_ff=opportunityOff:false
  14. VERIFY: opportunity page loads
  15. visit localhost:3000/dev/feature-flags
  16. VERIFY: both authOff and opportunityOff are disabled
  17. turn on both authOff and opportunityOff from the UI
  18. refresh
  19. VERIFY: sign in button appears
  20. visit http://localhost:3000/opportunity/33
  21. VERIFY: you are redirected to maintenance

Additional information

With auth enabled the header should look like this

Screenshot 2025-01-02 at 3 02 18 PM

@doug-s-nava doug-s-nava force-pushed the feature-flag-manager-refactor branch from 3438d42 to fb188d1 Compare January 2, 2025 16:35
@doug-s-nava doug-s-nava force-pushed the feature-flag-manager-refactor branch from 8a26362 to 971a129 Compare January 2, 2025 17:20
@doug-s-nava doug-s-nava marked this pull request as ready for review January 2, 2025 18:00

return (
// Stick the footer to the bottom of the page
<div className="display-flex flex-column minh-viewport">
<a className="usa-skipnav" href="#main-content">
{t("Layout.skip_to_main")}
</a>
<NextIntlClientProvider
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this change is unrelated but at one point I was messing with this file a lot and removing this unnecessary code simplified things for me a bit

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, good catch, this is already accounted for in the parent component

@@ -27,6 +30,21 @@ type Props = {

const homeRegexp = /^\/(?:e[ns])?$/;

const LoginLink = () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

should be removed on rebase to include actual login button

const { user, isLoading, error } = useUser();

if (!mounted) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

mounted is no longer a thing

} = process.env;

export const featureFlags = {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this could be done elsewhere if we'd rather keep this file clean

Copy link
Collaborator

Choose a reason for hiding this comment

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

They touch the envars so seems good to me.


// a workaround, as setting this in default state value results in hydration error
// on feature flag admin page. Does it cause a blip on other pages?
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this seems fine but it's a workaround just for the admin page so it may not be necessary

);
});

// // do we still need to support this?
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

may need some discussion here

@doug-s-nava doug-s-nava force-pushed the feature-flag-manager-refactor branch 2 times, most recently from 3988586 to 03f25d3 Compare January 2, 2025 20:03
@doug-s-nava doug-s-nava changed the title Feature flag manager refactor [Issue #3249] Feature flag manager refactor Jan 2, 2025
searchParams?: ServerSideSearchParams,
): boolean {
if (!isValidFeatureFlag(name)) {
throw new Error(`\`${name}\` is not a valid feature flag`);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just wondering about this down the road when we want to retire a feature flag like we did with search. I think this is throwing an error early while we're in that process and maybe missed a check because it's this throws when consuming the flag, not when trying to parse what is stuck in the user's cookies? Just trying to make sure we don't make it where we can't ever kill an old flag because some users have it set true and we will break them if it's removed from the defaults.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

that's an interesting point. The way things are structured though, I'm not too worried about this causing a problem. The calls to this function should always be coming with feature flag names set internal to the app, not with names read dynamically from cookies or query params. The only issue I can see is if a feature flag was not fully removed - if we remove a flag from the list of defaults but are still attempting to use it to gate behavior, for instance.

In the case where we remove a flag but a user's cookies are still set, I think the concern is that their cookie value wouldn't be honored, but that's probably not a big issue.

In any case it's existing behavior, so we can make another ticket to look into it if you think that's warranted.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks, I agree structurally we're ok, and I don't think we need to worry about not honoring them anymore once we've "dropped them" on the configurations side.

@doug-s-nava doug-s-nava force-pushed the feature-flag-manager-refactor branch from 03f25d3 to b8f15d0 Compare January 3, 2025 21:28

const { checkFeatureFlag } = result.current;

const value = checkFeatureFlag("someFakeFeature4");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit, but maybe making this "nonexistantFakeFeature" or "fakeFeatureDoesNotExist" as I could see 4 getting picked up by someone wanting to extend these tests further than then that would break this test. They'd realize the problem eventually, but might be easier to try to avoid the clash proactively.

@mdragon
Copy link
Collaborator

mdragon commented Jan 6, 2025

One thought, maybe for a follow up ticket. I've had issues in the past with end users getting odd stuff in their FF or even just App cookies and it can be helpful to have a URL that will just wipe out whatever they've got out there, basically just wipe clean the cookie. Often this happens on a Logout URL to other cookies, but FF ones in particular are usually not easily cleared by a non-technical user and potentially will break stuff all over the site if they're not working right. Might be worth throwing another URL arg on the flag manager or adding a _ff_clear URL argument that the middleware? would respond to by just setting an empty cookie over whatever the user had stuck in there. That way the help desk could send them a link with that special URL argument rather than as set of instructions for how to clear cookies on this site.

import Header from "./Header";
import Footer from "src/components/Footer";
import GrantsIdentifier from "src/components/GrantsIdentifier";
import Header from "src/components/Header";
Copy link
Collaborator

Choose a reason for hiding this comment

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

We had discussed when creating #2192 that we'd allow imports that are relative if they are in the same directory. We could revisit that, but would be good to update the es lint https://github.com/HHS/simpler-grants-gov/blob/main/frontend/.eslintrc.js#L22 so we are consistent.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

got it, forgot that we were allowing that. Happy to revert this

SESSION_SECRET = "",
FEATURE_SEARCH_OFF,
FEATURE_OPPORTUNITY_OFF,
FEATURE_AUTH_OFF,
Copy link
Collaborator

Choose a reason for hiding this comment

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

This feels cleaner to grab them here and set defaults in the export. Would like to alphabetize them as well, might do that in a follow-up PR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

agree alphabetizing is a good idea, also agree it might be cleaner to do it later on

@acouch
Copy link
Collaborator

acouch commented Jan 6, 2025

One issue I'm seeing with the authOff cookie is that it flickers for users who are not using the feature flag, which would be most people visiting the site. We could fix that by defaulting to off for the auth components and renaming the flag to authOn . The login link would flicker for those using it, but that seems more acceptable as they would know about it and could be assured it is temporary (both until we go live with auth but also if we deploy the CDN first).

@doug-s-nava
Copy link
Collaborator Author

@mdragon if I'm understanding your idea correctly, I think we already have it implemented here:

. If you enter a query param of _ff=reset it should reset the feature flags. As far as what they reset to, currently it's defaults, and with this change it would be { ...defaults, ...flag values from env vars }

isFeatureEnabled(
name: string,
cookies: NextRequest["cookies"] | ReadonlyRequestCookies,
searchParams?: ServerSideSearchParams,
Copy link
Collaborator

Choose a reason for hiding this comment

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

These are query params, not just for the search page AFAICT, so queryParams as an arg would make more sense IMO.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

not sure if you're saying that there is a better type to be using here or that we should rename the parameter. The nomenclature in the app is a bit confusing, and I confused myself enough while thinking about this that I had to look things up. https://stackoverflow.com/a/39294675 It sounds like "query params" or "search params" are not defined in any spec, but Next uses the term "search params" in their "useSearchParams" hook, and that's backed up by terminology in the URL API. "Query params" is definitely a very common phrase though. Not sure which way we'd want to go

Copy link
Collaborator

Choose a reason for hiding this comment

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

I meant the param name. Not sure why next uses "search" but imagine it is b/c they want to make it easier for new devs to create search pages.

@mdragon
Copy link
Collaborator

mdragon commented Jan 6, 2025

@mdragon if I'm understanding your idea correctly, I think we already have it implemented here:

Yup, exactly that, I didn't notice it was already in there.

Copy link
Collaborator

@acouch acouch left a comment

Choose a reason for hiding this comment

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

Looks great! Though I would address the flicker for the login link, either in this PR or other ticket.

@doug-s-nava doug-s-nava merged commit e358419 into feature/nextjs-auth Jan 6, 2025
14 checks passed
@doug-s-nava doug-s-nava deleted the feature-flag-manager-refactor branch January 6, 2025 20:55
doug-s-nava added a commit that referenced this pull request Jan 7, 2025
…em for client side use (#3374)

* rewrites the FeatureFlagManager and useFeatureFlag hook to allow syncing flags between server and client using cookies
* adds the `authOn` feature flag in frontend code and terraform
* refactors the `environments` setup a bit to more easily expose feature flags
* splits functionality that does not benefit from being held in the FeatureFlagsManager class into a helper file
* moves feature flag manager file into a nested directory
acouch pushed a commit that referenced this pull request Jan 13, 2025
…em for client side use (#3374)

* rewrites the FeatureFlagManager and useFeatureFlag hook to allow syncing flags between server and client using cookies
* adds the `authOn` feature flag in frontend code and terraform
* refactors the `environments` setup a bit to more easily expose feature flags
* splits functionality that does not benefit from being held in the FeatureFlagsManager class into a helper file
* moves feature flag manager file into a nested directory
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants