feat(devops): separate public page server timing diagnostics
CI / check (push) Successful in 2m23s
CI / deploy (push) Successful in 1m13s
CI / publish-container (push) Successful in 44s

This commit is contained in:
Simo committed 2026-09-11 10:49:08 +02:00
1 parent d5091d1a73
commit baeb54aeb5
15 files changed
+315 -29

No files matched your search

+4 -1
View File
@@ -7,6 +7,7 @@ import { ContentCard, EmptyState } from "@/components/public/ui";
import { auth } from "@/lib/auth";
import { excerpt, slugify } from "@/lib/format";
import { logger } from "@/lib/logger";
import { withPublicPagePerformance } from "@/lib/public-page-performance";
import { loadPublicEvents } from "@/lib/services/public-events";
export async function generateMetadata(): Promise<Metadata> {
@@ -22,7 +23,7 @@ export async function generateMetadata(): Promise<Metadata> {
};
}
export default async function EventsPage({
async function EventsPage({
searchParams,
}: {
searchParams: Promise<{
@@ -274,3 +275,5 @@ export default async function EventsPage({
</main>
);
}
export default withPublicPagePerformance("/events", EventsPage);
+4 -1
View File
@@ -11,6 +11,7 @@ import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { SurfaceCard } from "@/components/surface-card";
import { auth } from "@/lib/auth";
import { avatarImageUrl, slugify } from "@/lib/format";
import { withPublicPagePerformance } from "@/lib/public-page-performance";
import { loadUserDashboard } from "@/lib/services/user-dashboard";
import CopyReferralButton from "./CopyReferralButton";
import DashboardTasks from "./DashboardTasks";
@@ -21,7 +22,7 @@ export async function generateMetadata() {
return { title: t("title") };
}
export default async function MePage({
async function MePage({
searchParams,
}: {
searchParams: Promise<{ claimed?: string; error?: string }>;
@@ -440,3 +441,5 @@ export default async function MePage({
</main>
);
}
export default withPublicPagePerformance("/me", MePage);
+4 -1
View File
@@ -19,6 +19,7 @@ import {
} from "@/lib/db";
import { excerpt } from "@/lib/format";
import { formatDate } from "@/lib/format-date";
import { withPublicPagePerformance } from "@/lib/public-page-performance";
import { sanitize } from "@/lib/sanitize";
import { cacheNews } from "@/lib/services/news-cache";
@@ -92,7 +93,7 @@ const REACTIONS: { key: string; emoji: string }[] = [
{ key: "wow", emoji: "😮" },
];
export default async function ArticlePage({
async function ArticlePage({
params,
searchParams,
}: {
@@ -407,3 +408,5 @@ export default async function ArticlePage({
</main>
);
}
export default withPublicPagePerformance("/news/[slug]", ArticlePage);
+4 -1
View File
@@ -5,11 +5,12 @@ import { ContentCard, EmptyState } from "@/components/public/ui";
import { excerpt } from "@/lib/format";
import { formatDate } from "@/lib/format-date";
import { logger } from "@/lib/logger";
import { withPublicPagePerformance } from "@/lib/public-page-performance";
import { loadNewsArchive } from "@/lib/services/news-archive";
export const metadata = { title: "News" };
export default async function NewsPage({
async function NewsPage({
searchParams,
}: {
searchParams: Promise<{ q?: string; page?: string; order?: string }>;
@@ -188,3 +189,5 @@ export default async function NewsPage({
</main>
);
}
export default withPublicPagePerformance("/news", NewsPage);
+4 -5
View File
@@ -4,6 +4,7 @@ import Link from "@/components/link";
import { LocalEventTime } from "@/components/public/local-event-time";
import { ContentCard, EmptyState } from "@/components/public/ui";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { withPublicPagePerformance } from "@/lib/public-page-performance";
import { loadPublicSearch } from "@/lib/services/public-search";
export async function generateMetadata(): Promise<Metadata> {
@@ -17,11 +18,7 @@ type Params = {
newsPage?: string;
eventsPage?: string;
};
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<Params>;
}) {
async function SearchPage({ searchParams }: { searchParams: Promise<Params> }) {
const t = await getTranslations("pages.search");
const params = await searchParams;
const result = await loadPublicSearch({
@@ -226,3 +223,5 @@ export default async function SearchPage({
</main>
);
}
export default withPublicPagePerformance("/search", SearchPage);
+4 -1
View File
@@ -25,6 +25,7 @@ import {
} from "@/lib/db";
import { avatarImageUrl } from "@/lib/format";
import { formatDate } from "@/lib/format-date";
import { withPublicPagePerformance } from "@/lib/public-page-performance";
import { loadProfilePrivacy } from "@/lib/services/profile-privacy";
import { ProfilePhotoGallery } from "./ProfilePhotoGallery";
import styles from "./profile.module.css";
@@ -77,7 +78,7 @@ const BADGE_IMG_BASE = "https://images.habbo.com/c_images/album1584";
const CURRENCY_DUCKETS = 0;
const CURRENCY_DIAMONDS = 5;
export default async function ProfilePage({
async function ProfilePage({
params,
searchParams,
}: {
@@ -699,3 +700,5 @@ export default async function ProfilePage({
</main>
);
}
export default withPublicPagePerformance("/u/[username]", ProfilePage);
+37 -1
View File
@@ -1,4 +1,5 @@
import { createTranslator } from "next-intl";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { expect, it, vi } from "vitest";
import messages from "@/messages/en.json";
@@ -18,7 +19,9 @@ vi.mock("next/navigation", () => ({
},
}));
vi.mock("@/lib/performance-store", () => ({ readPerformance: mocks.read }));
vi.mock("@/components/link", () => ({ default: () => null }));
vi.mock("@/components/link", () => ({
default: ({ href }: { href: string }) => createElement("a", { href }),
}));
vi.mock("next-intl/server", () => ({
getTranslations: async () =>
@@ -75,3 +78,36 @@ it("renders filtered slow records and explains incomplete streaming timings", as
expect(html).toContain("Streaming response");
expect(html).toContain("Local diagnostics");
});
it("separates public component outcomes from HTTP response codes", async () => {
mocks.allowed = true;
mocks.read.mockResolvedValue({
shared: false,
samples: [
{
kind: "public-page",
outcome: "redirect",
operationId: "public-op",
route: "/me",
method: "PAGE",
status: 200,
at: 1000,
streaming: false,
metrics: {
durationMs: 2,
database: { count: 0, durationMs: 0, errors: 0 },
external: { count: 0, durationMs: 0, errors: 0 },
},
},
],
});
const html = renderToStaticMarkup(
await Page({ searchParams: Promise.resolve({ kind: "public-page" }) }),
);
expect(mocks.read).toHaveBeenLastCalledWith("public-page");
expect(html).toContain("Redirect");
expect(html).toContain(
"/admin/devops/performance?kind=public-page&amp;q=&amp;sort=slowest&amp;slow=0",
);
expect(html).not.toContain("HTTP 200");
expect(html).toContain("browser rendering");
});
+29 -4
View File
@@ -13,9 +13,16 @@ export default async function PerformancePage({
if (!canAccess(permissions, PERMS.DEVOPS_VIEW, session.user.rank))
redirect("/admin");
const t = await getTranslations("pages.admin.performance");
const { samples, shared } = await readPerformance();
const params = await searchParams;
const kind = params.kind === "public-page" ? "public-page" : "admin-api";
const { samples, shared } = await readPerformance(kind);
const query = (typeof params.q === "string" ? params.q : "").slice(0, 100);
const refreshParams = new URLSearchParams({
kind,
q: query,
sort: params.sort === "recent" ? "recent" : "slowest",
slow: params.slow === "1" ? "1" : "0",
});
const visible = samples.filter(
(sample) =>
`${sample.route} ${sample.operationId}`.includes(query) &&
@@ -36,7 +43,10 @@ export default async function PerformancePage({
<Link href="/admin/devops" className="btn btn-outline">
DevOps
</Link>
<Link href="/admin/devops/performance" className="btn btn-outline">
<Link
href={`/admin/devops/performance?${refreshParams}`}
className="btn btn-outline"
>
{t("refresh")}
</Link>
</div>
@@ -44,6 +54,15 @@ export default async function PerformancePage({
{t(shared ? "shared" : "local")} {t("retention")}
</p>
<form className="admin-card flex flex-wrap gap-3">
<select
name="kind"
defaultValue={kind}
aria-label={t("kind")}
className="input"
>
<option value="admin-api">{t("adminApi")}</option>
<option value="public-page">{t("publicPage")}</option>
</select>
<input
name="q"
defaultValue={query}
@@ -71,10 +90,13 @@ export default async function PerformancePage({
<option value="1">{t("slow")}</option>
</select>
<button className="btn btn-primary" type="submit">
{t("search")}
{t("applyFilters")}
</button>
</form>
<p className="text-sm text-muted-foreground">{t("explanation")}</p>
{kind === "public-page" && (
<p className="text-sm text-muted-foreground">{t("publicBoundary")}</p>
)}
{visible.length === 0 ? (
<p className="admin-card">{t("empty")}</p>
) : (
@@ -90,7 +112,10 @@ export default async function PerformancePage({
{sample.method} {sample.route}
</p>
<p className="text-sm text-muted-foreground">
{new Date(sample.at).toISOString()} · HTTP {sample.status}
{new Date(sample.at).toISOString()} ·{" "}
{sample.kind === "public-page"
? t(`outcomes.${sample.outcome ?? "completed"}`)
: `HTTP ${sample.status}`}
</p>
</div>
<strong