Compare commits

..

No commits in common. "e500cefcc2a5f801d8ccd6efd5c6a7c60e716dc4" and "3a5243fa3e980fa85c2312149af7bece2cc09023" have entirely different histories.

20 changed files with 2010 additions and 5 deletions

View file

@ -9,6 +9,10 @@ NEXT_PUBLIC_BASE_URL=http://localhost:3000
CLOUDFLARE_ZONE_ID=XXXXXXXXXXXXXXXX CLOUDFLARE_ZONE_ID=XXXXXXXXXXXXXXXX
CLOUDFLARE_API_TOKEN=XXXXXXXXXXXXXXXX CLOUDFLARE_API_TOKEN=XXXXXXXXXXXXXXXX
# Used for error tracking
NEXT_PUBLIC_SENTRY_DSN=""
SENTRY_URL=""
# Check Auth.js docs for information # Check Auth.js docs for information
AUTH_URL=http://localhost:3000 # This should be the same as NEXT_PUBLIC_BASE_URL AUTH_URL=http://localhost:3000 # This should be the same as NEXT_PUBLIC_BASE_URL
AUTH_TRUST_HOST=true AUTH_TRUST_HOST=true

2
.gitignore vendored
View file

@ -44,3 +44,5 @@ next-env.d.ts
# tomodachi-share # tomodachi-share
uploads/ uploads/
# Sentry Config File
.env.sentry-build-plugin

View file

@ -1,3 +1,4 @@
import { withSentryConfig } from "@sentry/nextjs";
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
@ -7,4 +8,41 @@ const nextConfig: NextConfig = {
}, },
}; };
export default nextConfig; export default withSentryConfig(nextConfig, {
// For all available options, see:
// https://www.npmjs.com/package/@sentry/webpack-plugin#options
org: "trafficlunar",
project: "tomodachishare",
sentryUrl: process.env.SENTRY_URL,
// Only print logs for uploading source maps in CI
silent: !process.env.CI,
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Uncomment to route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
// tunnelRoute: "/monitoring",
webpack: {
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
// See the following for more information:
// https://docs.sentry.io/product/crons/
// https://vercel.com/docs/cron-jobs
automaticVercelMonitors: false,
// Tree-shaking options for reducing bundle size
treeshake: {
// Automatically tree-shake Sentry logger statements to reduce bundle size
removeDebugLogging: true,
},
},
});

View file

@ -16,6 +16,7 @@
"@bprogress/next": "^3.2.12", "@bprogress/next": "^3.2.12",
"@hello-pangea/dnd": "^18.0.1", "@hello-pangea/dnd": "^18.0.1",
"@prisma/client": "^6.19.2", "@prisma/client": "^6.19.2",
"@sentry/nextjs": "^10.48.0",
"bit-buffer": "^0.3.0", "bit-buffer": "^0.3.0",
"canvas-confetti": "^1.9.4", "canvas-confetti": "^1.9.4",
"dayjs": "^1.11.20", "dayjs": "^1.11.20",

File diff suppressed because it is too large Load diff

16
sentry.server.config.ts Normal file
View file

@ -0,0 +1,16 @@
// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 0.1,
// Enable logs to be sent to Sentry
enableLogs: true,
// Enable sending user PII (Personally Identifiable Information)
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii
sendDefaultPii: false,
});

View file

@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import * as Sentry from "@sentry/nextjs";
import { profanity } from "@2toad/profanity"; import { profanity } from "@2toad/profanity";
import z from "zod"; import z from "zod";
@ -9,6 +10,7 @@ import { RateLimit } from "@/lib/rate-limit";
export async function PATCH(request: NextRequest) { export async function PATCH(request: NextRequest) {
const session = await auth(); const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
Sentry.setUser({ id: session.user?.id, name: session.user?.name });
const rateLimit = new RateLimit(request, 3); const rateLimit = new RateLimit(request, 3);
const check = await rateLimit.handle(); const check = await rateLimit.handle();
@ -27,6 +29,7 @@ export async function PATCH(request: NextRequest) {
}); });
} catch (error) { } catch (error) {
console.error("Failed to update description:", error); console.error("Failed to update description:", error);
Sentry.captureException(error, { extra: { stage: "update-about-me" } });
return rateLimit.sendResponse({ error: "Failed to update description" }, 500); return rateLimit.sendResponse({ error: "Failed to update description" }, 500);
} }

View file

@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import * as Sentry from "@sentry/nextjs";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
@ -7,6 +8,7 @@ import { RateLimit } from "@/lib/rate-limit";
export async function DELETE(request: NextRequest) { export async function DELETE(request: NextRequest) {
const session = await auth(); const session = await auth();
if (!session || !session.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); if (!session || !session.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
Sentry.setUser({ id: session.user.id, name: session.user.name });
const rateLimit = new RateLimit(request, 1); const rateLimit = new RateLimit(request, 1);
const check = await rateLimit.handle(); const check = await rateLimit.handle();
@ -18,6 +20,7 @@ export async function DELETE(request: NextRequest) {
}); });
} catch (error) { } catch (error) {
console.error("Failed to delete user:", error); console.error("Failed to delete user:", error);
Sentry.captureException(error, { extra: { stage: "delete-account" } });
return rateLimit.sendResponse({ error: "Failed to delete account" }, 500); return rateLimit.sendResponse({ error: "Failed to delete account" }, 500);
} }

View file

@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import * as Sentry from "@sentry/nextjs";
import { profanity } from "@2toad/profanity"; import { profanity } from "@2toad/profanity";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
@ -9,6 +10,7 @@ import { RateLimit } from "@/lib/rate-limit";
export async function PATCH(request: NextRequest) { export async function PATCH(request: NextRequest) {
const session = await auth(); const session = await auth();
if (!session || !session.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); if (!session || !session.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
Sentry.setUser({ id: session.user.id, name: session.user.name });
const rateLimit = new RateLimit(request, 3); const rateLimit = new RateLimit(request, 3);
const check = await rateLimit.handle(); const check = await rateLimit.handle();
@ -30,6 +32,7 @@ export async function PATCH(request: NextRequest) {
}); });
} catch (error) { } catch (error) {
console.error("Failed to update name:", error); console.error("Failed to update name:", error);
Sentry.captureException(error, { extra: { stage: "update-name" } });
return rateLimit.sendResponse({ error: "Failed to update name" }, 500); return rateLimit.sendResponse({ error: "Failed to update name" }, 500);
} }

View file

@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import * as Sentry from "@sentry/nextjs";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { z } from "zod"; import { z } from "zod";
@ -20,6 +21,7 @@ const formDataSchema = z.object({
export async function PATCH(request: NextRequest) { export async function PATCH(request: NextRequest) {
const session = await auth(); const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
Sentry.setUser({ id: session.user?.id, name: session.user?.name });
const rateLimit = new RateLimit(request, 3); const rateLimit = new RateLimit(request, 3);
const check = await rateLimit.handle(); const check = await rateLimit.handle();
@ -68,6 +70,7 @@ export async function PATCH(request: NextRequest) {
await fs.writeFile(fileLocation, pngBuffer); await fs.writeFile(fileLocation, pngBuffer);
} catch (error) { } catch (error) {
console.error("Error uploading profile picture:", error); console.error("Error uploading profile picture:", error);
Sentry.captureException(error, { extra: { stage: "upload-profile-picture" } });
return rateLimit.sendResponse({ error: "Failed to store profile picture" }, 500); return rateLimit.sendResponse({ error: "Failed to store profile picture" }, 500);
} }
@ -78,6 +81,7 @@ export async function PATCH(request: NextRequest) {
}); });
} catch (error) { } catch (error) {
console.error("Failed to update profile picture:", error); console.error("Failed to update profile picture:", error);
Sentry.captureException(error, { extra: { stage: "update-profile-picture" } });
return rateLimit.sendResponse({ error: "Failed to update profile picture" }, 500); return rateLimit.sendResponse({ error: "Failed to update profile picture" }, 500);
} }

View file

@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import * as Sentry from "@sentry/nextjs";
import fs from "fs/promises"; import fs from "fs/promises";
import path from "path"; import path from "path";
@ -13,6 +14,7 @@ const uploadsDirectory = path.join(process.cwd(), "uploads", "mii");
export async function DELETE(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { export async function DELETE(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await auth(); const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
Sentry.setUser({ id: session.user?.id, name: session.user?.name });
const rateLimit = new RateLimit(request, 30, "/api/mii/delete"); const rateLimit = new RateLimit(request, 30, "/api/mii/delete");
const check = await rateLimit.handle(); const check = await rateLimit.handle();
@ -42,6 +44,7 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
}); });
} catch (error) { } catch (error) {
console.error("Failed to delete Mii from database:", error); console.error("Failed to delete Mii from database:", error);
Sentry.captureException(error, { extra: { stage: "delete-mii" } });
return rateLimit.sendResponse({ error: "Failed to delete Mii" }, 500); return rateLimit.sendResponse({ error: "Failed to delete Mii" }, 500);
} }
@ -49,6 +52,7 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
await fs.rm(miiUploadsDirectory, { recursive: true, force: true }); await fs.rm(miiUploadsDirectory, { recursive: true, force: true });
} catch (error) { } catch (error) {
console.warn("Failed to delete Mii image files:", error); console.warn("Failed to delete Mii image files:", error);
Sentry.captureException(error, { extra: { stage: "delete-mii-images" } });
} }
return rateLimit.sendResponse({ success: true }); return rateLimit.sendResponse({ success: true });

View file

@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import * as Sentry from "@sentry/nextjs";
import { z } from "zod"; import { z } from "zod";
import { MiiGender, MiiMakeup, Prisma } from "@prisma/client"; import { Mii, MiiGender, MiiMakeup, Prisma } from "@prisma/client";
import fs from "fs/promises"; import fs from "fs/promises";
import path from "path"; import path from "path";
@ -45,6 +46,7 @@ const editSchema = z.object({
export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await auth(); const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
Sentry.setUser({ id: session.user?.id, name: session.user?.name });
const rateLimit = new RateLimit(request, 6); // no grouped pathname; edit each mii 2 times a minute const rateLimit = new RateLimit(request, 6); // no grouped pathname; edit each mii 2 times a minute
const check = await rateLimit.handle(); const check = await rateLimit.handle();
@ -190,6 +192,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
); );
} catch (error) { } catch (error) {
console.error("Error uploading user images:", error); console.error("Error uploading user images:", error);
Sentry.captureException(error, { extra: { stage: "edit-custom-images" } });
return rateLimit.sendResponse({ error: "Failed to store user images" }, 500); return rateLimit.sendResponse({ error: "Failed to store user images" }, 500);
} }
} }
@ -229,6 +232,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
); );
} catch (error) { } catch (error) {
console.error("Error uploading portrait/features images:", error); console.error("Error uploading portrait/features images:", error);
Sentry.captureException(error, { extra: { stage: "edit-portrait-features" } });
return rateLimit.sendResponse({ error: "Failed to store portrait/features images" }, 500); return rateLimit.sendResponse({ error: "Failed to store portrait/features images" }, 500);
} }
} }
@ -253,6 +257,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
}), }),
}).catch((err) => { }).catch((err) => {
console.error("Cloudflare cache purge failed:", err); console.error("Cloudflare cache purge failed:", err);
Sentry.captureException(err, { extra: { stage: "cloudflare-purge", miiId } });
}); });
return rateLimit.sendResponse({ success: true }); return rateLimit.sendResponse({ success: true });

View file

@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import * as Sentry from "@sentry/nextjs";
import { z } from "zod"; import { z } from "zod";
import { Prisma, ReportReason, ReportType } from "@prisma/client"; import { Prisma, ReportReason, ReportType } from "@prisma/client";
@ -18,6 +19,7 @@ const reportSchema = z.object({
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
const session = await auth(); const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
Sentry.setUser({ id: session.user?.id, name: session.user?.name });
const rateLimit = new RateLimit(request, 2); const rateLimit = new RateLimit(request, 2);
const check = await rateLimit.handle(); const check = await rateLimit.handle();
@ -83,6 +85,7 @@ export async function POST(request: NextRequest) {
}); });
} catch (error) { } catch (error) {
console.error("Report creation failed", error); console.error("Report creation failed", error);
Sentry.captureException(error, { extra: { stage: "create-report" } });
return rateLimit.sendResponse({ error: "Failed to create report" }, 500); return rateLimit.sendResponse({ error: "Failed to create report" }, 500);
} }

View file

@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import * as Sentry from "@sentry/nextjs";
import { z } from "zod"; import { z } from "zod";
import fs from "fs/promises"; import fs from "fs/promises";
@ -75,6 +76,7 @@ const submitSchema = z
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
const session = await auth(); const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
Sentry.setUser({ id: session.user?.id, name: session.user?.name });
const rateLimit = new RateLimit(request, 3); const rateLimit = new RateLimit(request, 3);
const check = await rateLimit.handle(); const check = await rateLimit.handle();
@ -90,6 +92,9 @@ export async function POST(request: NextRequest) {
rawTags = JSON.parse(formData.get("tags") as string); rawTags = JSON.parse(formData.get("tags") as string);
rawQrBytesRaw = JSON.parse(formData.get("qrBytesRaw") as string); rawQrBytesRaw = JSON.parse(formData.get("qrBytesRaw") as string);
} catch (error) { } catch (error) {
Sentry.captureException(error, {
extra: { stage: "submit-json-parse" },
});
return rateLimit.sendResponse({ error: "Invalid JSON in tags or QR code data" }, 400); return rateLimit.sendResponse({ error: "Invalid JSON in tags or QR code data" }, 400);
} }
@ -123,6 +128,15 @@ export async function POST(request: NextRequest) {
const firstIssue = parsed.error.issues[0]; const firstIssue = parsed.error.issues[0];
const path = firstIssue.path.length ? firstIssue.path.join(".") : "root"; const path = firstIssue.path.length ? firstIssue.path.join(".") : "root";
const error = `${path}: ${firstIssue.message}`; const error = `${path}: ${firstIssue.message}`;
const issues = parsed.error.issues;
const hasInstructionsErrors = issues.some((issue) => issue.path[0] === "instructions");
if (hasInstructionsErrors) {
Sentry.captureException(error, {
extra: { issues, rawInstructions: formData.get("instructions"), stage: "submit-instructions" },
});
}
return rateLimit.sendResponse({ error }, 400); return rateLimit.sendResponse({ error }, 400);
} }
const { const {
@ -178,6 +192,7 @@ export async function POST(request: NextRequest) {
try { try {
conversion = convertQrCode(qrBytes); conversion = convertQrCode(qrBytes);
} catch (error) { } catch (error) {
Sentry.captureException(error, { extra: { stage: "qr-conversion" } });
return rateLimit.sendResponse({ error: error instanceof Error ? error.message : String(error) }, 400); return rateLimit.sendResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
} }
} }
@ -262,6 +277,7 @@ export async function POST(request: NextRequest) {
await prisma.mii.delete({ where: { id: miiRecord.id } }); await prisma.mii.delete({ where: { id: miiRecord.id } });
console.error("Failed to download/store Mii portrait/features:", error); console.error("Failed to download/store Mii portrait/features:", error);
Sentry.captureException(error, { extra: { miiId: miiRecord.id, stage: "studio-image-download" } });
return rateLimit.sendResponse({ error: "Failed to download/store Mii portrait/features" }, 500); return rateLimit.sendResponse({ error: "Failed to download/store Mii portrait/features" }, 500);
} }
@ -269,6 +285,7 @@ export async function POST(request: NextRequest) {
await generateMetadataImage(miiRecord, session.user?.name!); await generateMetadataImage(miiRecord, session.user?.name!);
} catch (error) { } catch (error) {
console.error("Failed to generate metadata image:", error); console.error("Failed to generate metadata image:", error);
Sentry.captureException(error, { extra: { miiId: miiRecord.id, stage: "metadata-image-generation" } });
} }
if (platform === "THREE_DS") { if (platform === "THREE_DS") {
@ -294,6 +311,7 @@ export async function POST(request: NextRequest) {
await prisma.mii.delete({ where: { id: miiRecord.id } }); await prisma.mii.delete({ where: { id: miiRecord.id } });
console.error("Error processing Mii files:", error); console.error("Error processing Mii files:", error);
Sentry.captureException(error, { extra: { miiId: miiRecord.id, stage: "file-processing" } });
return rateLimit.sendResponse({ error: "Failed to process and store Mii files" }, 500); return rateLimit.sendResponse({ error: "Failed to process and store Mii files" }, 500);
} }
} }
@ -321,6 +339,8 @@ export async function POST(request: NextRequest) {
}); });
} catch (error) { } catch (error) {
console.error("Error storing user images:", error); console.error("Error storing user images:", error);
Sentry.captureException(error, { extra: { miiId: miiRecord.id, stage: "user-image-storage" } });
return rateLimit.sendResponse({ error: "Failed to store user images" }, 500); return rateLimit.sendResponse({ error: "Failed to store user images" }, 500);
} }

23
src/app/global-error.tsx Normal file
View file

@ -0,0 +1,23 @@
"use client";
import * as Sentry from "@sentry/nextjs";
import NextError from "next/error";
import { useEffect } from "react";
export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
useEffect(() => {
Sentry.captureException(error);
}, [error]);
return (
<html lang="en">
<body>
{/* `NextError` is the default Next.js error page component. Its type
definition requires a `statusCode` prop. However, since the App Router
does not expose status codes for errors, we simply pass 0 to render a
generic error message. */}
<NextError statusCode={0} />
</body>
</html>
);
}

View file

@ -10,7 +10,7 @@ export default function PrivacyPage() {
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl p-6"> <div className="bg-amber-50 border-2 border-amber-500 rounded-2xl p-6">
<h1 className="text-2xl font-bold">Privacy Policy</h1> <h1 className="text-2xl font-bold">Privacy Policy</h1>
<h2 className="font-light"> <h2 className="font-light">
<strong className="font-medium">Effective Date:</strong> 13 April 2026 <strong className="font-medium">Effective Date:</strong> 21 February 2026
</h2> </h2>
<hr className="border-black/20 mt-1 mb-4" /> <hr className="border-black/20 mt-1 mb-4" />
@ -65,6 +65,23 @@ export default function PrivacyPage() {
</p> </p>
</section> </section>
</li> </li>
<li>
<h3 className="text-xl font-semibold mt-6 mb-2">Error Reporting</h3>
<section>
<p className="mb-2">
This website uses{" "}
<a href="https://glitchtip.com/" className="text-blue-700">
GlitchTip
</a>{" "}
(a self-hosted Sentry-like instance) to monitor errors and site performance. To protect your privacy:
</p>
<ul className="list-disc list-inside ml-4">
<li>Errors and performance data is collected.</li>
<li>Only your user ID and name are sent, no other personally identifiable information is collected.</li>
<li>You can use ad blockers or browser privacy features to opt out.</li>
</ul>
</section>
</li>
<li> <li>
<h3 className="text-xl font-semibold mt-6 mb-2">Data Sharing</h3> <h3 className="text-xl font-semibold mt-6 mb-2">Data Sharing</h3>

View file

@ -0,0 +1,18 @@
// This file configures the initialization of Sentry on the client.
// The added config here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 0.1,
// Enable logs to be sent to Sentry
enableLogs: true,
// Enable sending user PII (Personally Identifiable Information)
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii
sendDefaultPii: false,
});
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;

9
src/instrumentation.ts Normal file
View file

@ -0,0 +1,9 @@
import * as Sentry from "@sentry/nextjs";
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("../sentry.server.config");
}
}
export const onRequestError = Sentry.captureRequestError;

View file

@ -4,6 +4,7 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import * as Sentry from "@sentry/nextjs";
import fs from "fs/promises"; import fs from "fs/promises";
import path from "path"; import path from "path";
@ -54,6 +55,7 @@ export async function validateImage(file: File): Promise<{ valid: boolean; error
return { valid: true }; return { valid: true };
} catch (error) { } catch (error) {
console.error("Error validating image:", error); console.error("Error validating image:", error);
Sentry.captureException(error, { extra: { stage: "image-validation" } });
return { valid: false, error: "Failed to process image file", status: 500 }; return { valid: false, error: "Failed to process image file", status: 500 };
} }
} }
@ -215,6 +217,7 @@ export async function generateMetadataImage(mii: Mii, author: string): Promise<{
await fs.writeFile(fileLocation, buffer); await fs.writeFile(fileLocation, buffer);
} catch (error) { } catch (error) {
console.error("Error storing 'metadata' image type", error); console.error("Error storing 'metadata' image type", error);
Sentry.captureException(error, { extra: { stage: "metadata-image-storage", miiId: mii.id } });
return { error: `Failed to store metadata image for ${mii.id}`, status: 500 }; return { error: `Failed to store metadata image for ${mii.id}`, status: 500 };
} }

View file

@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { createClient, RedisClientType } from "redis"; import { createClient, RedisClientType } from "redis";
import * as Sentry from "@sentry/nextjs";
import { auth } from "./auth"; import { auth } from "./auth";
const WINDOW_SIZE = 60; const WINDOW_SIZE = 60;
@ -19,6 +20,7 @@ async function getRedisClient() {
}); });
client.on("error", (error) => { client.on("error", (error) => {
console.error("Redis client error", error); console.error("Redis client error", error);
Sentry.captureException(error, { tags: { source: "redis-client" } });
}); });
await client.connect(); await client.connect();
} }
@ -69,6 +71,7 @@ export class RateLimit {
return { success, limit: this.maxRequests, remaining, expires: expireAt }; return { success, limit: this.maxRequests, remaining, expires: expireAt };
} catch (error) { } catch (error) {
console.error("Rate limit check failed", error); console.error("Rate limit check failed", error);
Sentry.captureException(error, { tags: { source: "rate-limit-check" } });
return { return {
success: false, success: false,
limit: this.maxRequests, limit: this.maxRequests,