feat(admin): add production error center and refresh CMS dependencies
This commit is contained in:
1 parent
c6b919c01d
commit
816e3875c2
47 files changed
+1545
-547
No files matched your search
@@ -0,0 +1,181 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import Link from "@/components/link";
|
||||
import { ErrorStore } from "@/lib/error-monitor";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { resolveCmsError } from "./actions";
|
||||
export default async function CmsErrorsPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<Record<string, string>>;
|
||||
}) {
|
||||
const { session, permissions } = await getAdminContext();
|
||||
if (!canAccess(permissions, PERMS.DEVOPS_VIEW, session.user.rank))
|
||||
redirect("/admin");
|
||||
const params = await searchParams;
|
||||
const query = (params.q ?? "").trim().slice(0, 200).toLowerCase();
|
||||
let result: Awaited<ReturnType<ErrorStore["read"]>>;
|
||||
try {
|
||||
result = await new ErrorStore().read();
|
||||
} catch {
|
||||
return (
|
||||
<div className="admin-card" role="alert">
|
||||
Error storage could not be read. Check the CMS storage volume and
|
||||
permissions.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const filtered = result.records.filter(
|
||||
(r) =>
|
||||
(!params.source || r.source === params.source) &&
|
||||
(!query || JSON.stringify(r).toLowerCase().includes(query)),
|
||||
);
|
||||
const groups = new Map<string, typeof filtered>();
|
||||
for (const r of filtered) {
|
||||
const list = groups.get(r.fingerprint) ?? [];
|
||||
list.push(r);
|
||||
groups.set(r.fingerprint, list);
|
||||
}
|
||||
const page = Math.max(
|
||||
1,
|
||||
Math.min(
|
||||
Number(params.page) || 1,
|
||||
Math.max(1, Math.ceil(groups.size / 25)),
|
||||
),
|
||||
);
|
||||
const pageHref = (next: number) =>
|
||||
`?${new URLSearchParams({ q: params.q ?? "", source: params.source ?? "", page: String(next) })}`;
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">CMS error center</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Server and browser errors from the last 7 days. Browser reports are
|
||||
unverified client observations.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link href="/admin/devops" className="btn btn-outline">
|
||||
DevOps
|
||||
</Link>
|
||||
<Link href="/admin/devops/errors" className="btn btn-outline">
|
||||
Emulator errors
|
||||
</Link>
|
||||
<Link href="/admin/devops/cms-errors" className="btn btn-outline">
|
||||
Refresh / clear filters
|
||||
</Link>
|
||||
</div>
|
||||
<form className="admin-card flex flex-wrap gap-3">
|
||||
<input
|
||||
aria-label="Search errors"
|
||||
name="q"
|
||||
defaultValue={params.q}
|
||||
placeholder="Reference, digest, message, route or release"
|
||||
className="input min-w-0 flex-1"
|
||||
/>
|
||||
<select
|
||||
name="source"
|
||||
aria-label="Error source"
|
||||
defaultValue={params.source ?? ""}
|
||||
className="input"
|
||||
>
|
||||
<option value="">All sources</option>
|
||||
<option value="server">Server</option>
|
||||
<option value="browser">Browser</option>
|
||||
</select>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Search
|
||||
</button>
|
||||
</form>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{filtered.length} occurrences · {groups.size} groups.{" "}
|
||||
{result.truncated
|
||||
? "Showing the latest 1,000 events. Narrowing filters searches this retained window."
|
||||
: ""}{" "}
|
||||
Storage is limited to 10 MB per day; excess events remain in server logs
|
||||
where available.
|
||||
</p>
|
||||
{groups.size === 0 ? (
|
||||
<div className="admin-card">
|
||||
No captured errors match these filters. Monitoring starts with this
|
||||
deployment; older console logs are not imported.
|
||||
</div>
|
||||
) : null}
|
||||
{[...groups.entries()]
|
||||
.slice((page - 1) * 25, page * 25)
|
||||
.map(([fingerprint, events]) => {
|
||||
const r = events[0];
|
||||
return (
|
||||
<details key={fingerprint} className="admin-card overflow-hidden">
|
||||
<summary className="cursor-pointer">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{r.resolved ? "Resolved" : "Open"} · {r.source} ·{" "}
|
||||
{events.length} occurrences · {r.at}
|
||||
</span>
|
||||
<p className="font-semibold break-words">{r.event}</p>
|
||||
<p className="text-sm break-words">{r.message}</p>
|
||||
</summary>
|
||||
<div className="mt-4 space-y-3 text-sm">
|
||||
<p>
|
||||
Reference: <code className="break-all">{r.id}</code>
|
||||
</p>
|
||||
<p>
|
||||
Release: <code className="break-all">{r.release}</code>
|
||||
</p>
|
||||
<pre className="whitespace-pre-wrap break-all rounded bg-muted p-3">
|
||||
{JSON.stringify(r.context, null, 2)}
|
||||
</pre>
|
||||
<pre className="max-h-96 overflow-auto whitespace-pre-wrap break-all rounded bg-muted p-3">
|
||||
{r.stack ||
|
||||
"No stack supplied. Use the reference or digest to correlate server logs."}
|
||||
</pre>
|
||||
{!r.resolved &&
|
||||
canAccess(permissions, PERMS.DEVOPS_EDIT, session.user.rank) ? (
|
||||
<form action={resolveCmsError}>
|
||||
<input
|
||||
type="hidden"
|
||||
name="fingerprint"
|
||||
value={r.fingerprint}
|
||||
/>
|
||||
<button type="submit" className="btn btn-outline">
|
||||
Mark resolved
|
||||
</button>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
A new occurrence reopens the group automatically.
|
||||
</p>
|
||||
</form>
|
||||
) : null}
|
||||
<details>
|
||||
<summary className="cursor-pointer">
|
||||
Occurrence references
|
||||
</summary>
|
||||
<ul>
|
||||
{events.map((e) => (
|
||||
<li key={e.id} className="break-all">
|
||||
{e.at} · {e.id}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
<p className="text-muted-foreground">
|
||||
Browser stack traces may reference compiled chunks. This view
|
||||
does not infer a root cause or include request bodies.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
})}
|
||||
<nav aria-label="Error pages" className="flex gap-3">
|
||||
{page > 1 ? (
|
||||
<Link href={pageHref(page - 1)} className="btn btn-outline">
|
||||
Previous
|
||||
</Link>
|
||||
) : null}
|
||||
{page * 25 < groups.size ? (
|
||||
<Link href={pageHref(page + 1)} className="btn btn-outline">
|
||||
Next
|
||||
</Link>
|
||||
) : null}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user