web: report server and browser errors to Bugsink

This commit is contained in:
ponas committed 2026-10-14 02:45:18 +02:00
1 parent b7e7847885
commit 6c324a48aa
10 files changed
+1685 -12

No files matched your search

+18 -1
View File
@@ -1,4 +1,5 @@
/** @type {import('next').NextConfig} */
import { withSentryConfig } from "@sentry/nextjs/config";
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -53,4 +54,20 @@ const nextConfig = {
},
};
export default nextConfig;
// Errors are reported to Bugsink (bugsink.neuf.no), which speaks Sentry's
// protocol. The build uploads source maps there when SENTRY_AUTH_TOKEN is
// set, and removes them from the output afterwards
export default withSentryConfig(nextConfig, {
sentryUrl: "https://bugsink.neuf.no/",
// Bugsink has no organisations; sentry-cli insists on a value
org: "bugsinkhasnoorgs",
project: "neuf-www",
authToken: process.env.SENTRY_AUTH_TOKEN,
silent: !process.env.CI,
telemetry: false,
sourcemaps: { deleteSourcemapsAfterUpload: true },
// the hook it asks for is for tracing navigations, which is off
suppressOnRouterTransitionStartWarning: true,
// Bugsink only takes errors: leave the tracing code out of the bundles
bundleSizeOptimizations: { excludeDebugStatements: true, excludeTracing: true },
});
+1597 -7
View File
File diff suppressed because it is too large. Load diff
+2
View File
@@ -21,6 +21,8 @@
"@graphql-codegen/cli": "^7.4.6",
"@graphql-codegen/client-preset": "^6.2.3",
"@parcel/watcher": "^2.6.0",
"@sentry/browser": "^11.7.0",
"@sentry/nextjs": "^11.7.0",
"@sindresorhus/slugify": "^3.0.1",
"@urql/next": "^2.0.1",
"date-fns": "^4.4.0",
+5
View File
@@ -3,11 +3,16 @@
import { useEffect } from "react";
import { PageHeader } from "@/components/general/PageHeader";
import { isChunkLoadError } from "@/lib/chunkError";
import { reportError } from "@/lib/reportError";
const RELOAD_KEY = "chunk-error-reload";
const RELOAD_COOLDOWN_MS = 10_000;
function ErrorPage({ error }: { error: Error & { digest?: string } }) {
useEffect(() => {
reportError(error);
}, [error]);
useEffect(() => {
if (!isChunkLoadError(error)) return;
// No reload loop if the chunk is gone for good
+9
View File
@@ -0,0 +1,9 @@
import { reportError } from "@/lib/reportError";
// Errors that no React error boundary catches
window.addEventListener("error", (event) => {
reportError(event.error ?? event.message);
});
window.addEventListener("unhandledrejection", (event) => {
reportError(event.reason);
});
+4
View File
@@ -6,6 +6,10 @@ vi.mock("@/lib/internalRevalidate", () => ({
setInternalRevalidateSecret: vi.fn(),
}));
// the real SDK is not under test, and pulls in all of Node's instrumentation
vi.mock("@sentry/nextjs", () => ({ captureRequestError: vi.fn() }));
vi.mock("./sentry.server.config", () => ({}));
import { register } from "./instrumentation.ts";
beforeEach(() => {
+4
View File
@@ -1,5 +1,9 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// the real SDK is not under test, and pulls in all of Node's instrumentation
vi.mock("@sentry/nextjs", () => ({ captureRequestError: vi.fn() }));
vi.mock("./sentry.server.config", () => ({}));
import { internalRevalidateSecret } from "@/lib/internalRevalidate";
import { register } from "./instrumentation.ts";
+14 -4
View File
@@ -1,3 +1,4 @@
import * as Sentry from "@sentry/nextjs";
import { osloDay } from "@/lib/date";
import {
purgeOwnCache,
@@ -84,11 +85,20 @@ function keepCacheCurrent() {
void check();
}
export function register() {
// only in the running production server: not during `next build`, in
// `next dev` (nothing is kept between requests there) or on the edge
// Reports errors thrown while rendering or handling a request to Bugsink
export const onRequestError = Sentry.captureRequestError;
export async function register() {
if (process.env.NEXT_RUNTIME !== "nodejs") return;
keepCacheCurrentInProduction();
await import("./sentry.server.config");
}
function keepCacheCurrentInProduction() {
// only in the running production server: not during `next build` or in
// `next dev` (nothing is kept between requests there)
if (
process.env.NEXT_RUNTIME !== "nodejs" ||
process.env.NODE_ENV !== "production" ||
process.env.NEXT_PHASE === "phase-production-build"
) {
+23
View File
@@ -0,0 +1,23 @@
// Sends an error from the browser to Bugsink. The SDK is only downloaded
// once there is something to report, so it costs nothing on a normal visit
let sdk: Promise<typeof import("@sentry/browser")> | undefined;
export function reportError(error: unknown) {
const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN;
// unset (as in local builds) leaves error reporting off
if (!dsn || process.env.NODE_ENV !== "production") return;
sdk ??= import("@sentry/browser").then((Sentry) => {
Sentry.init({
dsn,
// Bugsink takes errors only
tracesSampleRate: 0,
// instrumentation-client.ts listens for uncaught errors itself, from
// before the SDK is loaded
integrations: (defaults) =>
defaults.filter((integration) => integration.name !== "GlobalHandlers"),
});
return Sentry;
});
void sdk.then((Sentry) => Sentry.captureException(error));
}
+9
View File
@@ -0,0 +1,9 @@
import * as Sentry from "@sentry/nextjs";
Sentry.init({
// unset (as in local builds) leaves error reporting off
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
enabled: process.env.NODE_ENV === "production",
// Bugsink takes errors only
tracesSampleRate: 0,
});