web: report server and browser errors to Bugsink
This commit is contained in:
10 files changed
+1685
-12
No files matched your search
+18
-1
@@ -1,4 +1,5 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
|
import { withSentryConfig } from "@sentry/nextjs/config";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.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 },
|
||||||
|
});
|
||||||
Generated
+1597
-7
File diff suppressed because it is too large.
Load diff
@@ -21,6 +21,8 @@
|
|||||||
"@graphql-codegen/cli": "^7.4.6",
|
"@graphql-codegen/cli": "^7.4.6",
|
||||||
"@graphql-codegen/client-preset": "^6.2.3",
|
"@graphql-codegen/client-preset": "^6.2.3",
|
||||||
"@parcel/watcher": "^2.6.0",
|
"@parcel/watcher": "^2.6.0",
|
||||||
|
"@sentry/browser": "^11.7.0",
|
||||||
|
"@sentry/nextjs": "^11.7.0",
|
||||||
"@sindresorhus/slugify": "^3.0.1",
|
"@sindresorhus/slugify": "^3.0.1",
|
||||||
"@urql/next": "^2.0.1",
|
"@urql/next": "^2.0.1",
|
||||||
"date-fns": "^4.4.0",
|
"date-fns": "^4.4.0",
|
||||||
|
|||||||
@@ -3,11 +3,16 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { PageHeader } from "@/components/general/PageHeader";
|
import { PageHeader } from "@/components/general/PageHeader";
|
||||||
import { isChunkLoadError } from "@/lib/chunkError";
|
import { isChunkLoadError } from "@/lib/chunkError";
|
||||||
|
import { reportError } from "@/lib/reportError";
|
||||||
|
|
||||||
const RELOAD_KEY = "chunk-error-reload";
|
const RELOAD_KEY = "chunk-error-reload";
|
||||||
const RELOAD_COOLDOWN_MS = 10_000;
|
const RELOAD_COOLDOWN_MS = 10_000;
|
||||||
|
|
||||||
function ErrorPage({ error }: { error: Error & { digest?: string } }) {
|
function ErrorPage({ error }: { error: Error & { digest?: string } }) {
|
||||||
|
useEffect(() => {
|
||||||
|
reportError(error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isChunkLoadError(error)) return;
|
if (!isChunkLoadError(error)) return;
|
||||||
// No reload loop if the chunk is gone for good
|
// No reload loop if the chunk is gone for good
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
@@ -6,6 +6,10 @@ vi.mock("@/lib/internalRevalidate", () => ({
|
|||||||
setInternalRevalidateSecret: vi.fn(),
|
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";
|
import { register } from "./instrumentation.ts";
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
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 { internalRevalidateSecret } from "@/lib/internalRevalidate";
|
||||||
import { register } from "./instrumentation.ts";
|
import { register } from "./instrumentation.ts";
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
import { osloDay } from "@/lib/date";
|
import { osloDay } from "@/lib/date";
|
||||||
import {
|
import {
|
||||||
purgeOwnCache,
|
purgeOwnCache,
|
||||||
@@ -84,11 +85,20 @@ function keepCacheCurrent() {
|
|||||||
void check();
|
void check();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function register() {
|
// Reports errors thrown while rendering or handling a request to Bugsink
|
||||||
// only in the running production server: not during `next build`, in
|
export const onRequestError = Sentry.captureRequestError;
|
||||||
// `next dev` (nothing is kept between requests there) or on the edge
|
|
||||||
|
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 (
|
if (
|
||||||
process.env.NEXT_RUNTIME !== "nodejs" ||
|
|
||||||
process.env.NODE_ENV !== "production" ||
|
process.env.NODE_ENV !== "production" ||
|
||||||
process.env.NEXT_PHASE === "phase-production-build"
|
process.env.NEXT_PHASE === "phase-production-build"
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
Reference in new issue
Block a user