Skip to content

Commit 7c7d5f5

Browse files
aliggsawyerh
andauthored
Add sample feature flagging system to next.js template (#259)
## Changes - Add a service class for retrieving flags from evidently - Add example consuming the flags - Updated docs - tests ## Context for reviewers - This is followup / related work alongside last week's platform infra changes. ## Testing - I'd love some help testing this out against the platform infra. --------- Co-authored-by: Sawyer <[email protected]>
1 parent 526ef47 commit 7c7d5f5

15 files changed

+2220
-6
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
.DS_Store
33
# Developer-specific IDE settings
44
.vscode
5+
.env.local

app/.env.development

+6
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,9 @@
55

66
# If you deploy to a subpath, change this to the subpath so relative paths work correctly.
77
NEXT_PUBLIC_BASE_PATH=
8+
9+
# AWS Evidently Feature Flag variables
10+
AWS_ACCESS_KEY_ID=
11+
AWS_SECRET_ACCESS_KEY=
12+
FEATURE_FLAGS_PROJECT=
13+
AWS_REGION=

app/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -161,4 +161,5 @@ Optionally, configure your code editor to auto run these tools on file save. Mos
161161
## Other topics
162162

163163
- [Internationalization](../docs/internationalization.md)
164+
- [Feature flags](../docs/feature-flags.md)
164165
- Refer to the [architecture decision records](../docs/decisions) for more context on technical decisions.

app/jest.config.js

+7
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ const customJestConfig = {
1616
testEnvironment: "jsdom",
1717
// if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
1818
moduleDirectories: ["node_modules", "<rootDir>/"],
19+
moduleNameMapper: {
20+
// Force uuid to resolve with the CJS entry point ↴
21+
// See https://github.com/uuidjs/uuid/issues/451
22+
// This can be removed when @aws-sdk uses uuid v9+ ↴
23+
// https://github.com/aws/aws-sdk-js-v3/issues/3964
24+
uuid: require.resolve("uuid"),
25+
},
1926
};
2027

2128
module.exports = createJestConfig(customJestConfig);

app/package-lock.json

+2,053
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"ts:check": "tsc --noEmit"
2020
},
2121
"dependencies": {
22+
"@aws-sdk/client-evidently": "^3.465.0",
2223
"@trussworks/react-uswds": "^6.0.0",
2324
"@uswds/uswds": "3.7.0",
2425
"lodash": "^4.17.21",

app/src/i18n/messages/en-US/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ export const messages = {
1919
intro:
2020
"This is a template for a React web application using the <LinkToNextJs>Next.js framework</LinkToNextJs>.",
2121
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>",
22+
feature_flagging:
23+
"The template includes AWS Evidently for feature flagging. Toggle flag to see the content below change:",
24+
flag_off: "Flag is disabled",
25+
flag_on: "Flag is enabled",
2226
formatting:
2327
"The template includes an internationalization library with basic formatters built-in. Such as numbers: { amount, number, currency }, and dates: { isoDate, date, long}.",
2428
},

app/src/pages/index.tsx

+23-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
1-
import type { GetServerSideProps, NextPage } from "next";
1+
import type {
2+
GetServerSideProps,
3+
InferGetServerSidePropsType,
4+
NextPage,
5+
} from "next";
26
import { getMessagesWithFallbacks } from "src/i18n/getMessagesWithFallbacks";
7+
import { isFeatureEnabled } from "src/services/feature-flags";
38

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

7-
const Home: NextPage = () => {
12+
interface PageProps {
13+
isFooEnabled: boolean;
14+
}
15+
16+
const Home: NextPage<InferGetServerSidePropsType<typeof getServerSideProps>> = (
17+
props
18+
) => {
819
const t = useTranslations("home");
920

1021
return (
@@ -36,16 +47,25 @@ const Home: NextPage = () => {
3647
isoDate: new Date("2023-11-29T23:30:00.000Z"),
3748
})}
3849
</p>
50+
51+
{/* Demonstration of feature flagging */}
52+
<p>{t("feature_flagging")}</p>
53+
<p>{props.isFooEnabled ? t("flag_on") : t("flag_off")}</p>
3954
</div>
4055
</>
4156
);
4257
};
4358

4459
// Change this to getStaticProps if you're not using server-side rendering
45-
export const getServerSideProps: GetServerSideProps = async ({ locale }) => {
60+
export const getServerSideProps: GetServerSideProps<PageProps> = async ({
61+
locale,
62+
}) => {
63+
const isFooEnabled = await isFeatureEnabled("foo", "anonymous");
64+
4665
return {
4766
props: {
4867
messages: await getMessagesWithFallbacks(locale),
68+
isFooEnabled,
4969
},
5070
};
5171
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Evidently } from "@aws-sdk/client-evidently";
2+
3+
/**
4+
* Class for managing feature flagging via AWS Evidently.
5+
* Class method are available for use in next.js server side code.
6+
*
7+
* https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/evidently/
8+
*
9+
*/
10+
export class FeatureFlagManager {
11+
client: Evidently;
12+
private _project = process.env.FEATURE_FLAGS_PROJECT;
13+
14+
constructor() {
15+
this.client = new Evidently();
16+
}
17+
18+
async isFeatureEnabled(featureName: string, userId?: string) {
19+
const evalRequest = {
20+
entityId: userId,
21+
feature: featureName,
22+
project: this._project,
23+
};
24+
25+
let featureFlagValue = false;
26+
try {
27+
const evaluation = await this.client.evaluateFeature(evalRequest);
28+
if (evaluation && evaluation.value?.boolValue !== undefined) {
29+
featureFlagValue = evaluation.value.boolValue;
30+
console.log({
31+
message: "Made feature flag evaluation with AWS Evidently",
32+
data: {
33+
reason: evaluation.reason,
34+
userId: userId,
35+
featureName: featureName,
36+
featureFlagValue: featureFlagValue,
37+
},
38+
});
39+
}
40+
} catch (e) {
41+
console.error({
42+
message: "Error retrieving feature flag variation from AWS Evidently",
43+
data: {
44+
err: e,
45+
userId: userId,
46+
featureName: featureName,
47+
},
48+
});
49+
}
50+
return featureFlagValue;
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export class LocalFeatureFlagManager {
2+
async isFeatureEnabled(featureName: string, userId: string) {
3+
console.log("Using mock feature flag manager", { featureName, userId });
4+
return Promise.resolve(false);
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { LocalFeatureFlagManager } from "../LocalFeatureFlagManager";
2+
import type { FlagManager } from "../setup";
3+
4+
export const manager: FlagManager = new LocalFeatureFlagManager();
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { manager } from "./setup";
2+
3+
export function isFeatureEnabled(feature: string, userId?: string) {
4+
return manager.isFeatureEnabled(feature, userId);
5+
}
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { FeatureFlagManager } from "./FeatureFlagManager";
2+
import { LocalFeatureFlagManager } from "./LocalFeatureFlagManager";
3+
4+
export interface FlagManager {
5+
isFeatureEnabled(feature: string, userId?: string): Promise<boolean>;
6+
}
7+
8+
export const manager: FlagManager = process.env.FEATURE_FLAGS_PROJECT
9+
? new FeatureFlagManager()
10+
: new LocalFeatureFlagManager();

app/tests/pages/index.test.tsx

+23-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { axe } from "jest-axe";
2-
import Index from "src/pages/index";
2+
import { GetServerSidePropsContext } from "next";
3+
import Index, { getServerSideProps } from "src/pages/index";
4+
import { LocalFeatureFlagManager } from "src/services/feature-flags/LocalFeatureFlagManager";
35
import { render, screen } from "tests/react-utils";
46

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

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

@@ -15,9 +17,27 @@ describe("Index", () => {
1517
});
1618

1719
it("passes accessibility scan", async () => {
18-
const { container } = render(<Index />);
20+
const { container } = render(<Index isFooEnabled={true} />);
1921
const results = await axe(container);
2022

2123
expect(results).toHaveNoViolations();
2224
});
25+
26+
it("conditionally displays content based on feature flag values", () => {
27+
const { container } = render(<Index isFooEnabled={true} />);
28+
expect(container).toHaveTextContent("Flag is enabled");
29+
});
30+
31+
it("retrieves feature flags", async () => {
32+
const featureName = "foo";
33+
const userId = "anonymous";
34+
const featureFlagSpy = jest
35+
.spyOn(LocalFeatureFlagManager.prototype, "isFeatureEnabled")
36+
.mockResolvedValue(true);
37+
await getServerSideProps({
38+
req: { cookies: {} },
39+
res: {},
40+
} as unknown as GetServerSidePropsContext);
41+
expect(featureFlagSpy).toHaveBeenCalledWith(featureName, userId);
42+
});
2343
});

docs/feature-flags.md

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Feature flagging
2+
3+
- [AWS Evidently](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Evidently.html) is used for feature flagging
4+
- For more information about the decision-making behind using Evidently, [this infra ADR is available](https://github.com/navapbc/template-infra/blob/68b2db42d06198cb070b0603e63a930db346309f/docs/decisions/infra/0010-feature-flags-system-design.md)
5+
- Additional documentation of the feature flagging solution is available in [infra docs](https://github.com/navapbc/template-infra/blob/main/docs/feature-flags.md)
6+
7+
## How it works
8+
9+
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
10+
1. Pages can call `isFeatureEnabled` from Next.js server side code and return the feature flag value to components as props.
11+
12+
## Local development
13+
14+
Out-of-the-box, local calls where `FEATURE_FLAGS_PROJECT` environment variable is unset will fall back to use `LocalFeatureFlagManager` which defaults flag values to `false`.
15+
16+
If you want to test Evidently locally, use your AWS IAM credentials. Once you set `FEATURE_FLAGS_PROJECT` and the AWS environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_REGION`) in `app/.env.local`, calls to Evidently will succeed.
17+
18+
## Creating a new feature flag
19+
20+
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).
21+
22+
## Toggling feature flags
23+
24+
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).

0 commit comments

Comments
 (0)