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

Maintenance feature flag #35

Merged
merged 2 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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({
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