+
+
+
+
+
+ Back to home
+
+
+
+ {error.digest !== undefined && (
+
+ Reference: {error.digest}
+
+ )}
+
+ );
+}
diff --git a/apps/frontend/app/layout.tsx b/apps/frontend/app/layout.tsx
index b1dc3e1..8aa8348 100644
--- a/apps/frontend/app/layout.tsx
+++ b/apps/frontend/app/layout.tsx
@@ -1,3 +1,4 @@
+import * as Sentry from '@sentry/nextjs';
import Image from 'next/image';
import './global.css';
import Link from 'next/link';
@@ -23,22 +24,29 @@ export default async function RootLayout({
}: {
children: React.ReactNode;
}) {
- const defaultGameTypes = await prisma.gameType.findMany({
- select: {
- name: true,
- playerCount: true,
- mapCount: true,
- },
- orderBy: [
- {
- playerCount: 'desc',
+ // error.tsx cannot catch root layout errors, so a failure here would take
+ // down every page at once. Degrade to empty tabs instead.
+ const defaultGameTypes = await prisma.gameType
+ .findMany({
+ select: {
+ name: true,
+ playerCount: true,
+ mapCount: true,
},
- {
- mapCount: 'desc',
- },
- ],
- take: 2,
- });
+ orderBy: [
+ {
+ playerCount: 'desc',
+ },
+ {
+ mapCount: 'desc',
+ },
+ ],
+ take: 2,
+ })
+ .catch((error) => {
+ Sentry.captureException(error);
+ return [];
+ });
return (
diff --git a/apps/frontend/app/server/[ip]/[port]/schema.ts b/apps/frontend/app/server/[ip]/[port]/schema.ts
index 44c78e3..6f2a2e5 100644
--- a/apps/frontend/app/server/[ip]/[port]/schema.ts
+++ b/apps/frontend/app/server/[ip]/[port]/schema.ts
@@ -3,5 +3,5 @@ import { decodeIp } from "../../../../utils/encoding";
export const paramsSchema = z.object({
ip: z.string().transform(decodeIp),
- port: z.coerce.number().int().positive(),
+ port: z.coerce.number().int().positive().max(65535).catch(0),
});
diff --git a/apps/frontend/sentry.client.config.ts b/apps/frontend/sentry.client.config.ts
index 459b4f6..8384be4 100644
--- a/apps/frontend/sentry.client.config.ts
+++ b/apps/frontend/sentry.client.config.ts
@@ -8,17 +8,15 @@ if (process.env.SENTRY_DSN) {
Sentry.init({
dsn: process.env.SENTRY_DSN,
- // Adjust this value in production, or use tracesSampler for greater control
- tracesSampleRate: 1,
+ tracesSampleRate: 0.01,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
- replaysOnErrorSampleRate: 1.0,
-
- // This sets the sample rate to be 10%. You may want this to be 100% while
- // in development and sample at a lower rate in production
- replaysSessionSampleRate: 0.1,
+ // Replay is the tightest quota on the free plan (~50/month), so record only
+ // sessions that actually hit an error, never a percentage of all sessions.
+ replaysOnErrorSampleRate: 0.1,
+ replaysSessionSampleRate: 0,
// You can remove this option if you're not planning to use the Sentry Session Replay feature:
integrations: [
diff --git a/apps/frontend/sentry.edge.config.ts b/apps/frontend/sentry.edge.config.ts
index 5bcbdf3..f710458 100644
--- a/apps/frontend/sentry.edge.config.ts
+++ b/apps/frontend/sentry.edge.config.ts
@@ -5,13 +5,15 @@
import * as Sentry from "@sentry/nextjs";
import prisma from "./utils/prisma";
+import { ignoreTransactions, tracesSampleRate, tracesSampler } from "./utils/sentrySampling";
if (process.env.SENTRY_DSN) {
Sentry.init({
dsn: process.env.SENTRY_DSN,
- // Adjust this value in production, or use tracesSampler for greater control
- tracesSampleRate: 1,
+ tracesSampleRate,
+ tracesSampler,
+ ignoreTransactions,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
diff --git a/apps/frontend/sentry.server.config.ts b/apps/frontend/sentry.server.config.ts
index b2327b2..9ef5a33 100644
--- a/apps/frontend/sentry.server.config.ts
+++ b/apps/frontend/sentry.server.config.ts
@@ -4,13 +4,15 @@
import * as Sentry from "@sentry/nextjs";
import prisma from "./utils/prisma";
+import { ignoreTransactions, tracesSampleRate, tracesSampler } from "./utils/sentrySampling";
if (process.env.SENTRY_DSN) {
Sentry.init({
dsn: process.env.SENTRY_DSN,
- // Adjust this value in production, or use tracesSampler for greater control
- tracesSampleRate: 1,
+ tracesSampleRate,
+ tracesSampler,
+ ignoreTransactions,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
diff --git a/apps/frontend/utils/encoding.ts b/apps/frontend/utils/encoding.ts
index 9cf08e4..1164aed 100644
--- a/apps/frontend/utils/encoding.ts
+++ b/apps/frontend/utils/encoding.ts
@@ -21,10 +21,16 @@ export function encodeString(str: string) {
}
}
+// Never throws: malformed percent-escapes fall back to the raw segment, which
+// matches no record, so the page 404s instead of raising a 500.
export function decodeString(str: string) {
- if (str.startsWith('_')) {
- return base64url.decode(str.slice(1));
- } else {
- return decodeURIComponent(str);
+ try {
+ if (str.startsWith('_')) {
+ return base64url.decode(str.slice(1));
+ } else {
+ return decodeURIComponent(str);
+ }
+ } catch {
+ return str;
}
}
diff --git a/apps/frontend/utils/sentrySampling.ts b/apps/frontend/utils/sentrySampling.ts
new file mode 100644
index 0000000..296dd4b
--- /dev/null
+++ b/apps/frontend/utils/sentrySampling.ts
@@ -0,0 +1,28 @@
+import { isbot } from 'isbot';
+import { z } from 'zod';
+
+const DEFAULT_TRACES_SAMPLE_RATE = 0.01;
+
+export const tracesSampleRate = z
+ .string()
+ .trim()
+ .min(1)
+ .pipe(z.coerce.number().min(0).max(1))
+ .catch(DEFAULT_TRACES_SAMPLE_RATE)
+ .parse(process.env.SENTRY_TRACES_SAMPLE_RATE);
+
+export const ignoreTransactions = [
+ '/monitoring',
+ '/_next/',
+ '/favicon.ico',
+ '/robots.txt',
+ '/sitemap',
+];
+
+type SamplingContext = {
+ request?: { headers?: Record