Skip to content

Commit

Permalink
Merge pull request #35 from SVendittelli/feat/feature-flags
Browse files Browse the repository at this point in the history
Maintenance feature flag
  • Loading branch information
SVendittelli authored Nov 14, 2023
2 parents 20d6f58 + 32c3b9d commit dff5144
Show file tree
Hide file tree
Showing 13 changed files with 186 additions and 4 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
max_timeout: 300
- name: Run Playwright tests
run: npx playwright test
run: npm run test:e2e
env:
BASE_URL: ${{ steps.waitForDeploy.outputs.url }}
EDGE_CONFIG: ${{ secrets.EDGE_CONFIG }}
VERCEL_ENV: preview
- uses: actions/upload-artifact@v3
if: always()
with:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
/.next/
/out/

# vercel
.vercel

# production
/build

Expand Down
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"conventionalCommits.scopes": ["next.js", "home"],
"conventionalCommits.scopes": ["next.js", "home", "maintenance"],
"files.associations": {
"*.css": "tailwindcss"
},
Expand Down
30 changes: 30 additions & 0 deletions app/api/flags/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { FeatureFlag } from "@/app/lib/flags";
import { env } from "@/env.mjs";
import { get } from "@vercel/edge-config";
import { NextResponse } from "next/server";

export const runtime = "edge";

export async function GET() {
if (!env.EDGE_CONFIG || !env.VERCEL_ENV) {
console.error(
"Missing environment variables for feature flags, both should be defined:",
{
EDGE_CONFIG: !!env.EDGE_CONFIG,
VERCEL_ENV: !!env.VERCEL_ENV,
},
);
}

const flags = await get<FeatureFlag>(env.VERCEL_ENV);

if (!flags) {
console.error("Missing feature flag configuration");
return NextResponse.json(
{ message: "Missing feature flag configuration" },
{ status: 500 },
);
}

return NextResponse.json(flags);
}
4 changes: 4 additions & 0 deletions app/lib/flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type FeatureFlag = {
beta: boolean;
maintenance: boolean;
};
37 changes: 37 additions & 0 deletions app/maintenance/page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { render, screen } from "@testing-library/react";
import Page from "./page";

const refresh = jest.fn();
jest.mock("next/navigation", () => ({
useRouter: () => ({
refresh,
}),
}));

describe("Maintenance", () => {
beforeEach(() => {
jest.clearAllMocks();
});

it("renders page title 'Under Maintenance'", () => {
render(<Page />);
const messageElement = screen.getByText(/Under Maintenance/);
expect(messageElement).toBeInTheDocument();
});

it("renders a message", () => {
render(<Page />);
const messageElement = screen.getByText(
/You can try to refresh the page to see if the issue is resolved./,
);
expect(messageElement).toBeInTheDocument();
});

it("renders a 'Refresh' button", () => {
render(<Page />);
const button = screen.getByText("Refresh");
expect(button).toBeInTheDocument();
button.click();
expect(refresh).toHaveBeenCalled();
});
});
21 changes: 21 additions & 0 deletions app/maintenance/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use client";
import { useRouter } from "next/navigation";

export default function Page() {
const { refresh } = useRouter();

return (
<main className="min-h-screen w-screen p-4 flex flex-col items-center text-center gap-6 prose prose-neutral prose-invert bg-gradient-to-b from-red-900 via-red-600 to-red-900">
<h1>Under Maintenance</h1>
<p className="bg-neutral-800 bg-opacity-90 p-2 rounded-md">
You can try to refresh the page to see if the issue is resolved.
</p>
<button
className="bg-neutral-950 enabled:hover:bg-neutral-700 font-bold py-2 px-4 rounded-full"
onClick={refresh}
>
Refresh
</button>
</main>
);
}
12 changes: 12 additions & 0 deletions e2e/api/flags/route.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { expect, test } from "@playwright/test";

test("should return the feature flags successfully", async ({ request }) => {
const flags = await request.get("/api/flags");
expect(flags.ok()).toBeTruthy();
expect(flags.status()).toBe(200);
// Note, playwright tests are run against the preview environment. Tests will fail if the preview feature flags are altered.
expect(await flags.json()).toEqual({

Check failure on line 8 in e2e/api/flags/route.spec.ts

View workflow job for this annotation

GitHub Actions / e2e-tests

[Mobile Chrome] › api/flags/route.spec.ts:3:5 › should return the feature flags successfully

1) [Mobile Chrome] › api/flags/route.spec.ts:3:5 › should return the feature flags successfully ── Error: expect(received).toEqual(expected) // deep equality - Expected - 1 + Received + 1 Object { - "beta": true, + "beta": false, "maintenance": false, } 6 | expect(flags.status()).toBe(200); 7 | // Note, playwright tests are run against the preview environment. Tests will fail if the preview feature flags are altered. > 8 | expect(await flags.json()).toEqual({ | ^ 9 | beta: true, 10 | maintenance: false, 11 | }); at /home/runner/work/screamer/screamer/e2e/api/flags/route.spec.ts:8:30

Check failure on line 8 in e2e/api/flags/route.spec.ts

View workflow job for this annotation

GitHub Actions / e2e-tests

[Mobile Chrome] › api/flags/route.spec.ts:3:5 › should return the feature flags successfully

1) [Mobile Chrome] › api/flags/route.spec.ts:3:5 › should return the feature flags successfully ── Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(received).toEqual(expected) // deep equality - Expected - 1 + Received + 1 Object { - "beta": true, + "beta": false, "maintenance": false, } 6 | expect(flags.status()).toBe(200); 7 | // Note, playwright tests are run against the preview environment. Tests will fail if the preview feature flags are altered. > 8 | expect(await flags.json()).toEqual({ | ^ 9 | beta: true, 10 | maintenance: false, 11 | }); at /home/runner/work/screamer/screamer/e2e/api/flags/route.spec.ts:8:30

Check failure on line 8 in e2e/api/flags/route.spec.ts

View workflow job for this annotation

GitHub Actions / e2e-tests

[Mobile Chrome] › api/flags/route.spec.ts:3:5 › should return the feature flags successfully

1) [Mobile Chrome] › api/flags/route.spec.ts:3:5 › should return the feature flags successfully ── Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(received).toEqual(expected) // deep equality - Expected - 1 + Received + 1 Object { - "beta": true, + "beta": false, "maintenance": false, } 6 | expect(flags.status()).toBe(200); 7 | // Note, playwright tests are run against the preview environment. Tests will fail if the preview feature flags are altered. > 8 | expect(await flags.json()).toEqual({ | ^ 9 | beta: true, 10 | maintenance: false, 11 | }); at /home/runner/work/screamer/screamer/e2e/api/flags/route.spec.ts:8:30

Check failure on line 8 in e2e/api/flags/route.spec.ts

View workflow job for this annotation

GitHub Actions / e2e-tests

[Mobile Safari] › api/flags/route.spec.ts:3:5 › should return the feature flags successfully

2) [Mobile Safari] › api/flags/route.spec.ts:3:5 › should return the feature flags successfully ── Error: expect(received).toEqual(expected) // deep equality - Expected - 1 + Received + 1 Object { - "beta": true, + "beta": false, "maintenance": false, } 6 | expect(flags.status()).toBe(200); 7 | // Note, playwright tests are run against the preview environment. Tests will fail if the preview feature flags are altered. > 8 | expect(await flags.json()).toEqual({ | ^ 9 | beta: true, 10 | maintenance: false, 11 | }); at /home/runner/work/screamer/screamer/e2e/api/flags/route.spec.ts:8:30

Check failure on line 8 in e2e/api/flags/route.spec.ts

View workflow job for this annotation

GitHub Actions / e2e-tests

[Mobile Safari] › api/flags/route.spec.ts:3:5 › should return the feature flags successfully

2) [Mobile Safari] › api/flags/route.spec.ts:3:5 › should return the feature flags successfully ── Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(received).toEqual(expected) // deep equality - Expected - 1 + Received + 1 Object { - "beta": true, + "beta": false, "maintenance": false, } 6 | expect(flags.status()).toBe(200); 7 | // Note, playwright tests are run against the preview environment. Tests will fail if the preview feature flags are altered. > 8 | expect(await flags.json()).toEqual({ | ^ 9 | beta: true, 10 | maintenance: false, 11 | }); at /home/runner/work/screamer/screamer/e2e/api/flags/route.spec.ts:8:30

Check failure on line 8 in e2e/api/flags/route.spec.ts

View workflow job for this annotation

GitHub Actions / e2e-tests

[Mobile Safari] › api/flags/route.spec.ts:3:5 › should return the feature flags successfully

2) [Mobile Safari] › api/flags/route.spec.ts:3:5 › should return the feature flags successfully ── Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(received).toEqual(expected) // deep equality - Expected - 1 + Received + 1 Object { - "beta": true, + "beta": false, "maintenance": false, } 6 | expect(flags.status()).toBe(200); 7 | // Note, playwright tests are run against the preview environment. Tests will fail if the preview feature flags are altered. > 8 | expect(await flags.json()).toEqual({ | ^ 9 | beta: true, 10 | maintenance: false, 11 | }); at /home/runner/work/screamer/screamer/e2e/api/flags/route.spec.ts:8:30

Check failure on line 8 in e2e/api/flags/route.spec.ts

View workflow job for this annotation

GitHub Actions / e2e-tests

[Microsoft Edge] › api/flags/route.spec.ts:3:5 › should return the feature flags successfully

3) [Microsoft Edge] › api/flags/route.spec.ts:3:5 › should return the feature flags successfully ─ Error: expect(received).toEqual(expected) // deep equality - Expected - 1 + Received + 1 Object { - "beta": true, + "beta": false, "maintenance": false, } 6 | expect(flags.status()).toBe(200); 7 | // Note, playwright tests are run against the preview environment. Tests will fail if the preview feature flags are altered. > 8 | expect(await flags.json()).toEqual({ | ^ 9 | beta: true, 10 | maintenance: false, 11 | }); at /home/runner/work/screamer/screamer/e2e/api/flags/route.spec.ts:8:30

Check failure on line 8 in e2e/api/flags/route.spec.ts

View workflow job for this annotation

GitHub Actions / e2e-tests

[Microsoft Edge] › api/flags/route.spec.ts:3:5 › should return the feature flags successfully

3) [Microsoft Edge] › api/flags/route.spec.ts:3:5 › should return the feature flags successfully ─ Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(received).toEqual(expected) // deep equality - Expected - 1 + Received + 1 Object { - "beta": true, + "beta": false, "maintenance": false, } 6 | expect(flags.status()).toBe(200); 7 | // Note, playwright tests are run against the preview environment. Tests will fail if the preview feature flags are altered. > 8 | expect(await flags.json()).toEqual({ | ^ 9 | beta: true, 10 | maintenance: false, 11 | }); at /home/runner/work/screamer/screamer/e2e/api/flags/route.spec.ts:8:30

Check failure on line 8 in e2e/api/flags/route.spec.ts

View workflow job for this annotation

GitHub Actions / e2e-tests

[Microsoft Edge] › api/flags/route.spec.ts:3:5 › should return the feature flags successfully

3) [Microsoft Edge] › api/flags/route.spec.ts:3:5 › should return the feature flags successfully ─ Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(received).toEqual(expected) // deep equality - Expected - 1 + Received + 1 Object { - "beta": true, + "beta": false, "maintenance": false, } 6 | expect(flags.status()).toBe(200); 7 | // Note, playwright tests are run against the preview environment. Tests will fail if the preview feature flags are altered. > 8 | expect(await flags.json()).toEqual({ | ^ 9 | beta: true, 10 | maintenance: false, 11 | }); at /home/runner/work/screamer/screamer/e2e/api/flags/route.spec.ts:8:30

Check failure on line 8 in e2e/api/flags/route.spec.ts

View workflow job for this annotation

GitHub Actions / e2e-tests

[Google Chrome] › api/flags/route.spec.ts:3:5 › should return the feature flags successfully

4) [Google Chrome] › api/flags/route.spec.ts:3:5 › should return the feature flags successfully ── Error: expect(received).toEqual(expected) // deep equality - Expected - 1 + Received + 1 Object { - "beta": true, + "beta": false, "maintenance": false, } 6 | expect(flags.status()).toBe(200); 7 | // Note, playwright tests are run against the preview environment. Tests will fail if the preview feature flags are altered. > 8 | expect(await flags.json()).toEqual({ | ^ 9 | beta: true, 10 | maintenance: false, 11 | }); at /home/runner/work/screamer/screamer/e2e/api/flags/route.spec.ts:8:30
beta: true,
maintenance: false,
});
});
6 changes: 6 additions & 0 deletions env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,19 @@ export const env = createEnv({
.string()
.optional()
.transform((value) => !!value && value !== "false" && value !== "0"),
EDGE_CONFIG: z.string().optional(),
PORT: z.coerce.number().default(3000),
VERCEL_ENV: z
.enum(["development", "preview", "production"])
.default("development"),
},
client: {},
runtimeEnv: {
ANALYZE: process.env.ANALYZE,
BASE_URL: process.env.BASE_URL,
CI: process.env.CI,
EDGE_CONFIG: process.env.EDGE_CONFIG,
VERCEL_ENV: process.env.VERCEL_ENV,
PORT: process.env.PORT,
},
});
47 changes: 47 additions & 0 deletions middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { FeatureFlag } from "@/app/lib/flags";
import { env } from "@/env.mjs";
import { get } from "@vercel/edge-config";
import { NextRequest, NextResponse } from "next/server";

export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - maintenance (maintenance page)
*/
"/((?!api|_next/static|_next/image|favicon.ico|maintenance).*)",
],
};

export async function middleware(req: NextRequest) {
if (!env.EDGE_CONFIG || !env.VERCEL_ENV) {
// If the environment variables are not defined, log an error and continue
console.error(
"Missing environment variables for feature flags, both should be defined:",
{
EDGE_CONFIG: !!env.EDGE_CONFIG,
VERCEL_ENV: !!env.VERCEL_ENV,
},
);
return NextResponse.next();
}

try {
// Check whether the maintenance page should be shown
const flags = await get<FeatureFlag>(env.VERCEL_ENV);

// If is in maintenance mode, point the url pathname to the maintenance page
if (flags?.maintenance) {
req.nextUrl.pathname = `/maintenance`;

// Rewrite to the url
return NextResponse.rewrite(req.nextUrl);
}
} catch (error) {
console.error(error);
}
}
17 changes: 17 additions & 0 deletions package-lock.json

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

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,23 @@
"analyze": "cross-env ANALYZE=true npm run build",
"build": "next build",
"dev": "next dev",
"dev:test": "cross-env VERCEL=preview npm run dev",
"format": "prettier --write .",
"preinstall": "npx npm-only-allow@latest --PM npm",
"lint": "next lint",
"prepare": "husky install",
"start": "next start",
"test": "jest --watch",
"test:ci": "jest --ci",
"test:e2e": "playwright test"
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui"
},
"lint-staged": {
"**/*": "prettier --write --ignore-unknown"
},
"dependencies": {
"@t3-oss/env-nextjs": "0.7.1",
"@vercel/edge-config": "0.4.1",
"clsx": "2.0.0",
"next": "14.0.2",
"react": "18.2.0",
Expand Down
2 changes: 1 addition & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export default defineConfig({
webServer: env.CI
? undefined
: {
command: "npm run dev",
command: "npm run dev:test",
url: baseURL,
timeout: 120 * 1000,
reuseExistingServer: true,
Expand Down

0 comments on commit dff5144

Please sign in to comment.