feat(devops): separate public page server timing diagnostics
This commit is contained in:
1 parent
d5091d1a73
commit
baeb54aeb5
15 files changed
+315
-29
No files matched your search
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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,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);
|
||||
@@ -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);
|
||||
@@ -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&q=&sort=slowest&slow=0",
|
||||
);
|
||||
expect(html).not.toContain("HTTP 200");
|
||||
expect(html).toContain("browser rendering");
|
||||
});
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user