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

Add sample feature flagging system to next.js template #259

Merged
merged 42 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
f9767d6
add FF manager
aligg Dec 2, 2023
353630f
format
aligg Dec 2, 2023
6e20779
tweaks
aligg Dec 2, 2023
ed8be4d
update logging
aligg Dec 2, 2023
4bbe4fc
format
aligg Dec 2, 2023
3f03b05
tweak config
aligg Dec 2, 2023
682ba3f
refactor a few things
aligg Dec 2, 2023
4c00a33
tweaking
aligg Dec 2, 2023
5608096
foo
aligg Dec 2, 2023
4bbaf3d
typing errs
aligg Dec 4, 2023
8e4ea30
add mocks
aligg Dec 4, 2023
121a839
format
aligg Dec 4, 2023
072e5c9
variable names, console err, addl pr feedback
aligg Dec 4, 2023
96a1067
remove region
aligg Dec 4, 2023
22af91b
format
aligg Dec 4, 2023
ee9ebc7
rename class
aligg Dec 5, 2023
5960077
a few more pr review comments
aligg Dec 5, 2023
b85be42
add docs
aligg Dec 5, 2023
211a22c
add back evidently mock for now
aligg Dec 5, 2023
19bfc25
add back mock w/ format
aligg Dec 5, 2023
14e7f66
rebase
aligg Dec 6, 2023
f3eb517
format
aligg Dec 6, 2023
ebd85a6
add additional structure for feature flagging
aligg Dec 6, 2023
5e7bbd3
linting
aligg Dec 6, 2023
d5bc09e
spurious patch file
aligg Dec 6, 2023
cc4f457
update docs
aligg Dec 6, 2023
ea37495
spurious patch file again
aligg Dec 6, 2023
4ef4d5f
tests
aligg Dec 7, 2023
baa0f9c
Merge branch 'main' of github.com:navapbc/template-application-nextjs…
sawyerh Dec 7, 2023
04958d5
rename
aligg Dec 7, 2023
e03f4bc
Update docs/feature-flagging.md
aligg Dec 7, 2023
e8033ee
Update docs/feature-flagging.md
aligg Dec 7, 2023
f8abfea
Update app/src/pages/index.tsx
aligg Dec 7, 2023
68da9d5
Update app/src/pages/index.tsx
aligg Dec 7, 2023
354b7ec
Update app/src/services/feature-flags/LocalFeatureFlagManager.ts
aligg Dec 7, 2023
a3768f2
rename feature-flags docs and camel case keys
aligg Dec 7, 2023
dcdf950
make userId optional and remove extra lockfile
aligg Dec 7, 2023
6b0d455
update docs, jest config, and typo on project env var
aligg Dec 8, 2023
6e0fe93
format
aligg Dec 8, 2023
90e4a95
Update docs/feature-flags.md
aligg Dec 8, 2023
e038ff4
update env.development file, remove line from test and update readme
aligg Dec 8, 2023
62b1f5b
add a test for get server side props
aligg Dec 8, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
.DS_Store
# Developer-specific IDE settings
.vscode
.env.local
3 changes: 3 additions & 0 deletions app/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@

# If you deploy to a subpath, change this to the subpath so relative paths work correctly.
NEXT_PUBLIC_BASE_PATH=

# AWS Evidently Feature Flag variables
aligg marked this conversation as resolved.
Show resolved Hide resolved
sawyerh marked this conversation as resolved.
Show resolved Hide resolved
FEATURE_FLAGS_PROJECT=
2,053 changes: 2,053 additions & 0 deletions app/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"ts:check": "tsc --noEmit"
},
"dependencies": {
"@aws-sdk/client-evidently": "^3.465.0",
"@trussworks/react-uswds": "^6.0.0",
"@uswds/uswds": "3.7.0",
"lodash": "^4.17.21",
Expand Down
4 changes: 4 additions & 0 deletions app/src/i18n/messages/en-US/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export const messages = {
intro:
"This is a template for a React web application using the <LinkToNextJs>Next.js framework</LinkToNextJs>.",
body: "This is template includes:<ul><li>Framework for server-side rendered, static, or hybrid React applications</li><li>TypeScript and React testing tools</li><li>U.S. Web Design System for themeable styling and a set of common components</li><li>Type checking, linting, and code formatting tools</li><li>Storybook for a frontend workshop environment</li></ul>",
featureflagging:
"The template includes AWS Evidently for feature flagging. Toggle flag to see the content below change:",
flagoff: "Flag is disabled",
flagon: "Flag is enabled",
aligg marked this conversation as resolved.
Show resolved Hide resolved
formatting:
"The template includes an internationalization library with basic formatters built-in. Such as numbers: { amount, number, currency }, and dates: { isoDate, date, long}.",
},
Expand Down
26 changes: 23 additions & 3 deletions app/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import type { GetServerSideProps, NextPage } from "next";
import type {
GetServerSideProps,
InferGetServerSidePropsType,
NextPage,
} from "next";
import { getMessagesWithFallbacks } from "src/i18n/getMessagesWithFallbacks";
import { isFeatureEnabled } from "src/services/feature-flags";

import { useTranslations } from "next-intl";
import Head from "next/head";

const Home: NextPage = () => {
interface PageProps {
isFooEnabled: boolean;
}

const Home: NextPage<InferGetServerSidePropsType<typeof getServerSideProps>> = (
props: PageProps
aligg marked this conversation as resolved.
Show resolved Hide resolved
) => {
const t = useTranslations("home");

return (
Expand Down Expand Up @@ -36,16 +47,25 @@ const Home: NextPage = () => {
isoDate: new Date("2023-11-29T23:30:00.000Z"),
})}
</p>

{/* Demonstration of feature flagging */}
<p>{t("featureflagging")}</p>
{props.isFooEnabled ? <p>^..^{t("flagon")}</p> : <p>{t("flagoff")}</p>}
aligg marked this conversation as resolved.
Show resolved Hide resolved
</div>
</>
);
};

// Change this to getStaticProps if you're not using server-side rendering
export const getServerSideProps: GetServerSideProps = async ({ locale }) => {
export const getServerSideProps: GetServerSideProps<PageProps> = async ({
locale,
}) => {
const isFooEnabled = await isFeatureEnabled("foo", "anonymous");

return {
props: {
messages: await getMessagesWithFallbacks(locale),
isFooEnabled,
},
};
};
Expand Down
52 changes: 52 additions & 0 deletions app/src/services/feature-flags/FeatureFlagManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Evidently } from "@aws-sdk/client-evidently";

/**
* Class for managing feature flagging via AWS Evidently.
* Class method are available for use in next.js server side code.
*
* https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/evidently/
*
*/
export class FeatureFlagManager {
client: Evidently;
private _project = process.env.FEATURE_FLAGS_PROJECT;

constructor() {
this.client = new Evidently();
}

async isFeatureEnabled(featureName: string, userId: string) {
aligg marked this conversation as resolved.
Show resolved Hide resolved
const evalRequest = {
entityId: userId,
feature: featureName,
project: this._project,
};

let featureFlagValue = false;
try {
const evaluation = await this.client.evaluateFeature(evalRequest);
if (evaluation && evaluation.value?.boolValue !== undefined) {
featureFlagValue = evaluation.value.boolValue;
console.log({
message: "Made feature flag evaluation with AWS Evidently",
data: {
reason: evaluation.reason,
userId: userId,
featureName: featureName,
featureFlagValue: featureFlagValue,
},
});
}
} catch (e) {
console.error({
message: "Error retrieving feature flag variation from AWS Evidently",
data: {
err: e,
userId: userId,
featureName: featureName,
},
});
}
return featureFlagValue;
}
}
8 changes: 8 additions & 0 deletions app/src/services/feature-flags/LocalFeatureFlagManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export class LocalFeatureFlagManager {
async isFeatureEnabled(featureName: string, userId: string) {
console.log(
`Using mock feature flag manager for feature ${featureName}, user ${userId}`
);
aligg marked this conversation as resolved.
Show resolved Hide resolved
return Promise.resolve(false);
}
}
4 changes: 4 additions & 0 deletions app/src/services/feature-flags/__mocks__/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { LocalFeatureFlagManager } from "../LocalFeatureFlagManager";
import type { FlagManager } from "../setup";

export const manager: FlagManager = new LocalFeatureFlagManager();
5 changes: 5 additions & 0 deletions app/src/services/feature-flags/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { manager } from "./setup";

export function isFeatureEnabled(feature: string, userId: string) {
return manager.isFeatureEnabled(feature, userId);
}
10 changes: 10 additions & 0 deletions app/src/services/feature-flags/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { FeatureFlagManager } from "./FeatureFlagManager";
import { LocalFeatureFlagManager } from "./LocalFeatureFlagManager";

export interface FlagManager {
isFeatureEnabled(feature: string, userId: string): Promise<boolean>;
}

export const manager: FlagManager = process.env.FEATURE_FLAG_PROJECT
? new FeatureFlagManager()
: new LocalFeatureFlagManager();
6 changes: 4 additions & 2 deletions app/tests/pages/index.test.tsx
aligg marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { axe } from "jest-axe";
import Index from "src/pages/index";
import { render, screen } from "tests/react-utils";

jest.mock("src/services/feature-flags/setup");
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can get rid of this line as well as the __mocks__ file now since the moduleNameMapper change fixed the original issue.


describe("Index", () => {
// Demonstration of rendering translated text, and asserting the presence of a dynamic value.
// You can delete this test for your own project.
it("renders link to Next.js docs", () => {
render(<Index />);
render(<Index isFooEnabled={true} />);

const link = screen.getByRole("link", { name: /next\.js/i });

Expand All @@ -15,7 +17,7 @@ describe("Index", () => {
});

it("passes accessibility scan", async () => {
const { container } = render(<Index />);
const { container } = render(<Index isFooEnabled={true} />);
const results = await axe(container);

expect(results).toHaveNoViolations();
Expand Down
22 changes: 22 additions & 0 deletions docs/feature-flagging.md
aligg marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Feature flagging

- [AWS Evidently](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Evidently.html) is used for feature flagging
- For more information about the decision-making behind using Evidently, [this infra ADR is available](https://github.com/navapbc/template-infra/blob/main/docs/decisions/infra/0010-feature-flags-system-design.md)
Copy link
Contributor

Choose a reason for hiding this comment

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

The PR is merged, so you can probably reference the ADR directly on the main branch

aligg marked this conversation as resolved.
Show resolved Hide resolved
- Additional documentation of the feature flagging solution is available in [infra docs](https://github.com/navapbc/template-infra/blob/main/docs/feature-flags.md)

## How it works

1. `services/feature-flags/FeatureFlagManager` provides a service layer to interact with AWS Evidently endpoints. For example, class method `isFeatureEnabled` calls out to Evidently to retrieve a feature flag value we can then return to the client
1. Pages can call `isFeatureEnabled` from next.js server side code and return the feature flag value to components as props.
aligg marked this conversation as resolved.
Show resolved Hide resolved

## Local development

Out-of-the-box, local calls where `FEATURE_FLAG_PROJECT` environment variable is unset will fall back to use `LocalFeatureFlagManager` which defaults flag values to false. If you want to test Evidently locally, use your AWS IAM credentials. Once you set AWS environment variables locally for the environment you wish to connect to, calls to Evidently will succeed
aligg marked this conversation as resolved.
Show resolved Hide resolved

## Creating a new feature flag

To create a new feature flag, update `/infra/[app_name]/app-config/main.tf`. More information available in infra repository [docs](https://github.com/navapbc/template-infra/blob/main/docs/feature-flags.md).

## Toggling feature flags

Toggle feature flags via the AWS Console GUI. More information [here](https://github.com/navapbc/template-infra/blob/main/docs/feature-flags.md#managing-feature-releases-and-partial-rollouts-via-aws-console).
6 changes: 6 additions & 0 deletions package-lock.json
aligg marked this conversation as resolved.
Show resolved Hide resolved

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