chore(dependencies): add Sentry to /ui (#8730)

Co-authored-by: Alan Buscaglia <gentlemanprogramming@gmail.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
This commit is contained in:
Alejandro Bailo
2025-11-11 17:12:42 +01:00
committed by GitHub
parent 0f22e754f2
commit ccb269caa2
22 changed files with 3938 additions and 422 deletions
+9
View File
@@ -14,6 +14,15 @@ UI_PORT=3000
AUTH_SECRET="N/c6mnaS5+SWq81+819OrzQZlmx1Vxtp/orjttJSmw8="
# Google Tag Manager ID
NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID=""
# Sentry
SENTRY_DSN=
NEXT_PUBLIC_SENTRY_DSN=
SENTRY_ORG=
SENTRY_PROJECT=
SENTRY_AUTH_TOKEN=
SENTRY_ENVIRONMENT=production
NEXT_PUBLIC_SENTRY_ENVIRONMENT=production
#### Code Review Configuration ####
# Enable Claude Code standards validation on pre-push hook
# Set to 'true' to validate changes against AGENTS.md standards via Claude Code
+3
View File
@@ -4,6 +4,7 @@ import { AuthError } from "next-auth";
import { signIn, signOut } from "@/auth.config";
import { apiBaseUrl } from "@/lib";
import { addAuthEvent } from "@/lib/sentry-breadcrumbs";
import type { SignInFormData, SignUpFormData } from "@/types";
export async function authenticate(
@@ -11,6 +12,7 @@ export async function authenticate(
formData: SignInFormData,
) {
try {
addAuthEvent("login", { email: formData.email });
await signIn("credentials", {
...formData,
redirect: false,
@@ -20,6 +22,7 @@ export async function authenticate(
};
} catch (error) {
if (error instanceof AuthError) {
addAuthEvent("error", { type: error.type });
switch (error.type) {
case "CredentialsSignin":
return {
+12 -1
View File
@@ -3,6 +3,7 @@
import { redirect } from "next/navigation";
import { apiBaseUrl, getAuthHeaders, getErrorMessage } from "@/lib";
import { addScanOperation } from "@/lib/sentry-breadcrumbs";
import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper";
export const getScans = async ({
@@ -89,6 +90,11 @@ export const scanOnDemand = async (formData: FormData) => {
return { error: "Provider ID is required" };
}
addScanOperation("create", undefined, {
provider_id: String(providerId),
scan_name: scanName ? String(scanName) : undefined,
});
const url = new URL(`${apiBaseUrl}/scans`);
try {
@@ -113,8 +119,13 @@ export const scanOnDemand = async (formData: FormData) => {
body: JSON.stringify(requestBody),
});
return handleApiResponse(response, "/scans");
const result = await handleApiResponse(response, "/scans");
if (result?.data?.id) {
addScanOperation("start", result.data.id);
}
return result;
} catch (error) {
addScanOperation("create");
return handleApiError(error);
}
};
+6
View File
@@ -1,6 +1,7 @@
"use server";
import { getTask } from "@/actions/task/tasks";
import { addTaskEvent } from "@/lib/sentry-breadcrumbs";
import type {
GetTaskResponse,
PollOptions,
@@ -14,10 +15,12 @@ export async function pollTaskUntilSettled<R = unknown>(
taskId: string,
{ maxAttempts = 10, delayMs = 2000 }: PollOptions = {},
): Promise<PollSettledResult<R>> {
addTaskEvent("started", taskId, { max_attempts: maxAttempts });
let attempts = 0;
while (attempts < maxAttempts) {
const resp = (await getTask(taskId)) as GetTaskResponse<R>;
if ("error" in resp) {
addTaskEvent("failed", taskId, { error: resp.error });
return { ok: false, error: resp.error };
}
const task = resp.data;
@@ -25,15 +28,18 @@ export async function pollTaskUntilSettled<R = unknown>(
const result = task?.attributes?.result;
if (!state) {
addTaskEvent("failed", taskId, { error: "Task state unavailable" });
return { ok: false, error: "Task state unavailable", task };
}
if (state !== "executing" && state !== "available") {
addTaskEvent("completed", taskId, { state });
return { ok: true, state, task, result };
}
attempts++;
await sleep(delayMs);
}
addTaskEvent("timeout", taskId, { attempts: attempts });
return { ok: false, error: "Task timeout" };
}
+33 -1
View File
@@ -1,11 +1,13 @@
"use client";
import { Icon } from "@iconify/react";
import * as Sentry from "@sentry/nextjs";
import { useEffect } from "react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui";
import { CustomButton } from "@/components/ui/custom";
import { CustomLink } from "@/components/ui/custom/custom-link";
import { SentryErrorSource, SentryErrorType } from "@/sentry";
export default function Error({
error,
@@ -29,9 +31,39 @@ export default function Error({
digest: error.digest,
timestamp: new Date().toISOString(),
});
// TODO: sent to sentry
// Send to Sentry with high priority
Sentry.captureException(error, {
tags: {
error_boundary: "app",
error_type: SentryErrorType.SERVER_ERROR,
error_source: SentryErrorSource.ERROR_BOUNDARY,
status_code: "500",
digest: error.digest,
},
level: "error",
fingerprint: ["server-error", error.message],
contexts: {
error_details: {
is_server_error: true,
timestamp: new Date().toISOString(),
},
},
});
} else {
console.error("Application error:", error);
// Send other errors to Sentry with normal priority
Sentry.captureException(error, {
tags: {
error_boundary: "app",
error_type: SentryErrorType.APPLICATION_ERROR,
error_source: SentryErrorSource.ERROR_BOUNDARY,
digest: error.digest,
},
level: "warning",
fingerprint: ["app-error", error.message],
});
}
}, [error]);
+4
View File
@@ -1,5 +1,6 @@
import "@/styles/globals.css";
import * as Sentry from "@sentry/nextjs";
import { Metadata, Viewport } from "next";
import React from "react";
@@ -22,6 +23,9 @@ export const metadata: Metadata = {
icons: {
icon: "/favicon.ico",
},
other: {
...Sentry.getTraceData(),
},
};
export const viewport: Viewport = {
+38 -1
View File
@@ -1,4 +1,5 @@
import { toUIMessageStream } from "@ai-sdk/langchain";
import * as Sentry from "@sentry/nextjs";
import { createUIMessageStreamResponse, UIMessage } from "ai";
import { getLighthouseConfig } from "@/actions/lighthouse/lighthouse";
@@ -6,6 +7,7 @@ import { getErrorMessage } from "@/lib/helper";
import { getCurrentDataSection } from "@/lib/lighthouse/data";
import { convertVercelMessageToLangChainMessage } from "@/lib/lighthouse/utils";
import { initLighthouseWorkflow } from "@/lib/lighthouse/workflow";
import { SentryErrorSource, SentryErrorType } from "@/sentry";
export async function POST(req: Request) {
try {
@@ -96,7 +98,23 @@ export async function POST(req: Request) {
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
// For errors, send a plain string that toUIMessageStream will convert to text chunks
// Capture stream processing errors
Sentry.captureException(error, {
tags: {
api_route: "lighthouse_analyst",
error_type: SentryErrorType.STREAM_PROCESSING,
error_source: SentryErrorSource.API_ROUTE,
},
level: "error",
contexts: {
lighthouse: {
event_type: "stream_error",
message_count: processedMessages.length,
},
},
});
controller.enqueue(`[LIGHTHOUSE_ANALYST_ERROR]: ${errorMessage}`);
controller.close();
}
@@ -109,6 +127,25 @@ export async function POST(req: Request) {
});
} catch (error) {
console.error("Error in POST request:", error);
// Capture API route errors
Sentry.captureException(error, {
tags: {
api_route: "lighthouse_analyst",
error_type: SentryErrorType.REQUEST_PROCESSING,
error_source: SentryErrorSource.API_ROUTE,
method: "POST",
},
level: "error",
contexts: {
request: {
method: req.method,
url: req.url,
headers: Object.fromEntries(req.headers.entries()),
},
},
});
return Response.json(
{ error: await getErrorMessage(error) },
{ status: 500 },
+40
View File
@@ -0,0 +1,40 @@
"use client";
import * as Sentry from "@sentry/nextjs";
import NextError from "next/error";
import { useEffect } from "react";
import { SentryErrorSource, SentryErrorType } from "@/sentry";
export default function GlobalError({
error,
reset: _reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
Sentry.captureException(error, {
tags: {
error_boundary: "global",
error_type: SentryErrorType.APPLICATION_ERROR,
error_source: SentryErrorSource.ERROR_BOUNDARY,
digest: error.digest,
},
level: "error",
contexts: {
react: {
componentStack: error.stack,
},
},
});
}, [error]);
return (
<html lang="en">
<body>
<NextError statusCode={500} />
</body>
</html>
);
}
+115
View File
@@ -0,0 +1,115 @@
/**
* Client-side Sentry instrumentation
*
* This file is automatically loaded by Next.js in the browser via the instrumentation hook.
* It configures Sentry for client-side error tracking and performance monitoring.
*
* For server-side configuration, see: instrumentation.ts
* For runtime-specific configs, see: sentry/sentry.server.config.ts and sentry/sentry.edge.config.ts
*/
import { browserTracingIntegration } from "@sentry/browser";
import * as Sentry from "@sentry/nextjs";
const isDevelopment = process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT === "dev";
/**
* Initialize Sentry error tracking and performance monitoring
*
* This setup includes:
* - Performance monitoring with Web Vitals tracking (LCP, FID, CLS, INP)
* - Long task detection for UI-blocking operations
* - beforeSend hook to filter noise
*/
Sentry.init({
// 📍 DSN - Data Source Name (identifies your Sentry project)
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// 🌍 Environment - Separate dev errors from production
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "local",
// 📦 Release - Track which version has the error
release: process.env.NEXT_PUBLIC_PROWLER_RELEASE_VERSION,
// 🐛 Debug - Detailed logs in development console
debug: isDevelopment,
// 📊 Sample Rates - Performance monitoring
// 100% in dev (test everything), 50% in production (balance visibility with costs)
tracesSampleRate: isDevelopment ? 1.0 : 0.5,
profilesSampleRate: isDevelopment ? 1.0 : 0.5,
// 🔌 Integrations
integrations: [
// 📊 Performance Monitoring: Core Web Vitals + RUM
// Tracks LCP, FID, CLS, INP
// Real User Monitoring captures actual user experience, not synthetic tests
browserTracingIntegration({
enableLongTask: true, // Detect tasks that block UI (>50ms)
enableInp: true, // Interaction to Next Paint (Core Web Vital)
}),
],
// 🎣 beforeSend Hook - Filter or modify events before sending to Sentry
ignoreErrors: [
// Browser extensions
"top.GLOBALS",
// Random network errors
"Network request failed",
"NetworkError",
"Failed to fetch",
// User canceled actions
"AbortError",
"Non-Error promise rejection captured",
// NextAuth expected errors
"NEXT_REDIRECT",
// ResizeObserver errors (common browser quirk, not real bugs)
"ResizeObserver",
],
beforeSend(event, hint) {
// Filter out noise: ResizeObserver errors (common browser quirk, not real bugs)
if (event.message?.includes("ResizeObserver")) {
return null; // Don't send to Sentry
}
// Filter out non-actionable errors
if (event.exception) {
const error = hint.originalException;
// Don't send cancelled requests
if (
error &&
typeof error === "object" &&
"name" in error &&
error.name === "AbortError"
) {
return null;
}
// Add additional context for API errors
if (
error &&
typeof error === "object" &&
"message" in error &&
typeof error.message === "string" &&
error.message.includes("Request failed")
) {
event.tags = {
...event.tags,
error_type: "api_error",
};
}
}
return event; // Send to Sentry
},
});
// 👤 Set user context (identifies who experienced the error)
// In production, this will be updated after authentication
if (isDevelopment) {
Sentry.setUser({
id: "dev-user",
});
}
+3
View File
@@ -1,5 +1,8 @@
"use client";
// Import Sentry client-side initialization
import "@/app/instrumentation.client";
import { HeroUIProvider } from "@heroui/system";
import { useRouter } from "next/navigation";
import { SessionProvider } from "next-auth/react";
+9 -1
View File
@@ -159,6 +159,14 @@
"strategy": "installed",
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
"name": "@sentry/nextjs",
"from": "10.11.0",
"to": "10.11.0",
"strategy": "installed",
"generatedAt": "2025-10-22T15:52:15.849Z"
},
{
"section": "dependencies",
"name": "@tailwindcss/postcss",
@@ -709,7 +717,7 @@
"from": "3.4.1",
"to": "3.4.1",
"strategy": "installed",
"generatedAt": "2025-10-22T12:36:37.962Z"
"generatedAt": "2025-10-22T15:52:15.849Z"
},
{
"section": "devDependencies",
+30
View File
@@ -0,0 +1,30 @@
/**
* Next.js Instrumentation Hook
*
* This file is automatically executed by Next.js at startup to initialize server-side SDKs.
*
* Configuration Flow:
* 1. This file (instrumentation.ts) - Server-side initialization
* 2. Runtime-specific configs:
* - sentry/sentry.server.config.ts (Node.js runtime)
* - sentry/sentry.edge.config.ts (Edge runtime)
* 3. Client-side init:
* - app/instrumentation.client.ts (Browser/Client)
*
* @see https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation
*/
import * as Sentry from "@sentry/nextjs";
export async function register() {
// The Sentry SDK automatically loads the appropriate config based on runtime
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("./sentry/sentry.server.config");
}
if (process.env.NEXT_RUNTIME === "edge") {
await import("./sentry/sentry.edge.config");
}
}
export const onRequestError = Sentry.captureRequestError;
+179
View File
@@ -0,0 +1,179 @@
/**
* Sentry Breadcrumb Utilities
*
* Provides helper functions to add breadcrumbs for tracking critical paths
* and user actions throughout the application.
*
* Usage:
* ```typescript
* import { addUserAction, addApiCall, addTaskEvent } from '@/lib/sentry-breadcrumbs';
*
* addUserAction('clicked_create_scan', { provider: 'aws' });
* addApiCall('POST /scans', 'success');
* addTaskEvent('scan_started', 'scan-123');
* ```
*/
import * as Sentry from "@sentry/nextjs";
export interface BreadcrumbContext {
[key: string]: string | number | boolean | undefined;
}
/**
* Add breadcrumb for user actions
* @param action - User action identifier
* @param context - Additional context data
*/
export function addUserAction(action: string, context?: BreadcrumbContext) {
Sentry.addBreadcrumb({
message: `User action: ${action}`,
category: "user.action",
level: "info",
data: context,
});
}
/**
* Add breadcrumb for API calls
* @param endpoint - API endpoint (e.g., "GET /scans")
* @param status - Status of the call (success, error, timeout)
* @param context - Additional context data
*/
export function addApiCall(
endpoint: string,
status: "success" | "error" | "timeout",
context?: BreadcrumbContext,
) {
Sentry.addBreadcrumb({
message: `API ${endpoint}`,
category: "api",
level: status === "error" ? "warning" : "info",
data: {
status,
...context,
},
});
}
/**
* Add breadcrumb for task events
* @param event - Task event (started, completed, failed)
* @param taskId - Task identifier
* @param context - Additional context data
*/
export function addTaskEvent(
event: "started" | "completed" | "failed" | "timeout",
taskId: string,
context?: BreadcrumbContext,
) {
Sentry.addBreadcrumb({
message: `Task ${event}: ${taskId}`,
category: "task",
level: event === "failed" ? "warning" : "info",
data: {
task_id: taskId,
...context,
},
});
}
/**
* Add breadcrumb for authentication events
* @param event - Auth event (login, logout, signup)
* @param context - Additional context data
*/
export function addAuthEvent(
event: "login" | "logout" | "signup" | "error",
context?: BreadcrumbContext,
) {
Sentry.addBreadcrumb({
message: `Auth event: ${event}`,
category: "auth",
level: event === "error" ? "warning" : "info",
data: context,
});
}
/**
* Add breadcrumb for form submissions
* @param formName - Name of the form
* @param status - Status of submission
* @param context - Additional context data
*/
export function addFormSubmission(
formName: string,
status: "started" | "success" | "error",
context?: BreadcrumbContext,
) {
Sentry.addBreadcrumb({
message: `Form submission: ${formName}`,
category: "form",
level: status === "error" ? "warning" : "info",
data: {
status,
...context,
},
});
}
/**
* Add breadcrumb for navigation
* @param from - Source path
* @param to - Destination path
*/
export function addNavigation(from: string, to: string) {
Sentry.addBreadcrumb({
message: `Navigation: ${from}${to}`,
category: "navigation",
level: "info",
});
}
/**
* Add breadcrumb for scan operations
* @param operation - Operation type (create, start, cancel, etc.)
* @param scanId - Scan identifier
* @param context - Additional context data
*/
export function addScanOperation(
operation: "create" | "start" | "cancel" | "pause" | "resume",
scanId?: string,
context?: BreadcrumbContext,
) {
Sentry.addBreadcrumb({
message: `Scan ${operation}${scanId ? `: ${scanId}` : ""}`,
category: "scan",
level: "info",
data: {
scan_id: scanId,
...context,
},
});
}
/**
* Add breadcrumb for data mutations
* @param entity - Entity type (provider, scan, role, etc.)
* @param action - Action type (create, update, delete)
* @param entityId - Entity identifier
* @param context - Additional context data
*/
export function addDataMutation(
entity: string,
action: "create" | "update" | "delete",
entityId?: string,
context?: BreadcrumbContext,
) {
Sentry.addBreadcrumb({
message: `Data mutation: ${action} ${entity}${entityId ? ` (${entityId})` : ""}`,
category: "data",
level: "info",
data: {
entity,
action,
entity_id: entityId,
...context,
},
});
}
+116 -4
View File
@@ -1,8 +1,14 @@
import * as Sentry from "@sentry/nextjs";
import { revalidatePath } from "next/cache";
import { SentryErrorSource, SentryErrorType } from "@/sentry";
import { getErrorMessage, parseStringify } from "./helper";
// Helper function to handle API responses consistently
/**
* Helper function to handle API responses consistently
* Includes Sentry error tracking for debugging
*/
export const handleApiResponse = async (
response: Response,
pathToRevalidate?: string,
@@ -29,12 +35,67 @@ export const handleApiResponse = async (
response.statusText ||
"Oops! Something went wrong.";
//5XX errors
// Capture error context for Sentry
const errorContext = {
status: response.status,
statusText: response.statusText,
url: response.url,
errorDetail,
pathToRevalidate,
};
// 5XX errors - Server errors (high priority)
if (response.status >= 500) {
throw new Error(
const serverError = new Error(
errorDetail ||
`Server error (${response.status}): The server encountered an error. Please try again later.`,
);
Sentry.captureException(serverError, {
tags: {
api_error: true,
status_code: response.status.toString(),
error_type: SentryErrorType.SERVER_ERROR,
error_source: SentryErrorSource.HANDLE_API_RESPONSE,
},
level: "error",
contexts: {
api_response: errorContext,
},
fingerprint: [
"api-server-error",
response.status.toString(),
response.url,
],
});
throw serverError;
}
// Client errors (4xx) - Only capture unexpected ones
if (![401, 403, 404].includes(response.status)) {
const clientError = new Error(
errorDetail ||
`Request failed (${response.status}): ${response.statusText}`,
);
Sentry.captureException(clientError, {
tags: {
api_error: true,
status_code: response.status.toString(),
error_type: SentryErrorType.CLIENT_ERROR,
error_source: SentryErrorSource.HANDLE_API_RESPONSE,
},
level: "warning",
contexts: {
api_response: errorContext,
},
fingerprint: [
"api-client-error",
response.status.toString(),
response.url,
],
});
}
return errorsArray
@@ -76,9 +137,60 @@ export const handleApiResponse = async (
return parse ? parseStringify(data) : data;
};
// Helper function to handle API errors consistently
/**
* Helper function to handle API errors consistently
* Includes Sentry error tracking
*/
export const handleApiError = (error: unknown): { error: string } => {
console.error(error);
// Check if this error was already captured by handleApiResponse
const isAlreadyCaptured =
error instanceof Error &&
(error.message.includes("Server error") ||
error.message.includes("Request failed"));
// Only capture if not already captured by handleApiResponse
if (!isAlreadyCaptured) {
if (error instanceof Error) {
// Don't capture expected errors
if (
!error.message.includes("401") &&
!error.message.includes("403") &&
!error.message.includes("404")
) {
Sentry.captureException(error, {
tags: {
error_source: SentryErrorSource.HANDLE_API_ERROR,
error_type: SentryErrorType.UNEXPECTED_ERROR,
},
level: "error",
contexts: {
error_details: {
message: error.message,
stack: error.stack,
},
},
});
}
} else {
// Capture non-Error objects
Sentry.captureMessage(
`Non-Error object in handleApiError: ${String(error)}`,
{
level: "warning",
tags: {
error_source: SentryErrorSource.HANDLE_API_ERROR,
error_type: SentryErrorType.NON_ERROR_OBJECT,
},
extra: {
error: error,
},
},
);
}
}
return {
error: getErrorMessage(error),
};
+61 -17
View File
@@ -1,19 +1,36 @@
const { withSentryConfig } = require("@sentry/nextjs");
/** @type {import('next').NextConfig} */
// HTTP Security Headers
// 'unsafe-eval' is configured under `script-src` because it is required by NextJS for development mode
const cspHeader = `
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://js.stripe.com https://www.googletagmanager.com;
connect-src 'self' https://api.iconify.design https://api.simplesvg.com https://api.unisvg.com https://js.stripe.com https://www.googletagmanager.com;
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://js.stripe.com https://www.googletagmanager.com https://browser.sentry-cdn.com;
connect-src 'self' https://api.iconify.design https://api.simplesvg.com https://api.unisvg.com https://js.stripe.com https://www.googletagmanager.com https://*.sentry.io https://*.ingest.sentry.io;
img-src 'self' https://www.google-analytics.com https://www.googletagmanager.com;
font-src 'self';
style-src 'self' 'unsafe-inline';
frame-src 'self' https://js.stripe.com https://www.googletagmanager.com;
frame-ancestors 'none';
report-to csp-endpoint;
`;
module.exports = {
// Get Sentry CSP report endpoint if DSN is configured
const getSentryReportEndpoint = () => {
if (!process.env.NEXT_PUBLIC_SENTRY_DSN) return null;
try {
const sentryKey =
process.env.NEXT_PUBLIC_SENTRY_DSN.split("@")[0]?.split("//")[1];
return sentryKey
? `https://o0.ingest.sentry.io/api/0/security/?sentry_key=${sentryKey}`
: null;
} catch {
return null;
}
};
const nextConfig = {
poweredByHeader: false,
// Use standalone only in production deployments, not for CI/testing
...(process.env.NODE_ENV === "production" &&
@@ -28,24 +45,51 @@ module.exports = {
root: __dirname,
},
async headers() {
const sentryEndpoint = getSentryReportEndpoint();
const headers = [
{
key: "Content-Security-Policy",
value: cspHeader.replace(/\n/g, ""),
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
];
// Add Reporting-Endpoints header if Sentry is configured
if (sentryEndpoint) {
headers.push({
key: "Reporting-Endpoints",
value: `csp-endpoint="${sentryEndpoint}"`,
});
}
return [
{
source: "/(.*)",
headers: [
{
key: "Content-Security-Policy",
value: cspHeader.replace(/\n/g, ""),
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
],
headers,
},
];
},
};
// Sentry configuration options
const sentryWebpackPluginOptions = {
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
silent: true, // Suppresses all logs
hideSourceMaps: true, // Hides source maps from generated client bundles
disableLogger: true, // Automatically tree-shake Sentry logger statements to reduce bundle size
widenClientFileUpload: true, // Upload a larger set of source maps for prettier stack traces
};
// Export with Sentry only if configuration is available
module.exports = process.env.SENTRY_DSN
? withSentryConfig(nextConfig, sentryWebpackPluginOptions)
: nextConfig;
+3045 -397
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -43,6 +43,7 @@
"@radix-ui/react-toast": "1.2.14",
"@react-aria/ssr": "3.9.4",
"@react-aria/visually-hidden": "3.8.12",
"@sentry/nextjs": "10.11.0",
"@tailwindcss/postcss": "4.1.13",
"@tailwindcss/typography": "0.5.16",
"@tanstack/react-table": "8.21.3",
+52
View File
@@ -0,0 +1,52 @@
# Sentry Error Tracking Configuration
This folder contains all Sentry-related configuration and utilities for the Prowler UI.
## Files
- `sentry.server.config.ts` - Server-side error tracking configuration
- `sentry.edge.config.ts` - Edge runtime error tracking configuration
- `utils.ts` - Enums for standardized error types and sources
- `index.ts` - Main export file
## Client Configuration
The client-side configuration is located in `app/instrumentation.client.ts` following Next.js conventions.
## Usage
```typescript
// Import Sentry enums for error categorization
import { SentryErrorType, SentryErrorSource } from "@/sentry";
// Use in error handling
Sentry.captureException(error, {
tags: {
error_type: SentryErrorType.SERVER_ERROR,
error_source: SentryErrorSource.API_ROUTE,
},
});
```
## Environment Variables
Required environment variables (add to `.env`):
```env
SENTRY_DSN=https://YOUR_KEY@o0.ingest.sentry.io/0
NEXT_PUBLIC_SENTRY_DSN=https://YOUR_KEY@o0.ingest.sentry.io/0
SENTRY_ORG=your-org-slug
SENTRY_PROJECT=your-project-slug
SENTRY_AUTH_TOKEN=sntrys_YOUR_AUTH_TOKEN
SENTRY_ENVIRONMENT=development
NEXT_PUBLIC_SENTRY_ENVIRONMENT=development
```
## Ignored Errors
The following errors are intentionally ignored as they are expected behavior:
- `NEXT_REDIRECT` - Next.js redirect mechanism
- `NEXT_NOT_FOUND` - Next.js 404 handling
- `401` - Unauthorized (expected when token expires)
- `403` - Forbidden (expected for permission checks)
- `404` - Not Found (expected for missing resources)
+2
View File
@@ -0,0 +1,2 @@
// Re-export all Sentry utilities
export * from "./utils";
+64
View File
@@ -0,0 +1,64 @@
import * as Sentry from "@sentry/nextjs";
const isProduction = process.env.SENTRY_ENVIRONMENT === "pro";
/**
* Edge runtime Sentry configuration
*
* Edge runtime has stricter constraints than Node.js:
* - Limited execution time (~10-30 seconds)
* - Lower memory availability
* - Reduced sample rates to minimize overhead
* - No complex integrations
*/
Sentry.init({
// 📍 DSN - Data Source Name (identifies your Sentry project)
dsn: process.env.SENTRY_DSN,
// 🌍 Environment configuration
environment: process.env.SENTRY_ENVIRONMENT || "local",
// 📦 Release tracking
release: process.env.SENTRY_RELEASE,
// 📊 Sample Rates - Reduced for edge runtime constraints
// 50% in dev, 25% in production (edge has lower overhead limits than server)
tracesSampleRate: isProduction ? 0.25 : 0.5,
// 🔌 Integrations - Edge runtime doesn't support all integrations
integrations: [],
// 🎣 Filter expected errors - Don't send noise to Sentry
ignoreErrors: [
// NextAuth redirect errors - Expected behavior in auth flow
"NEXT_REDIRECT",
"NEXT_NOT_FOUND",
// Expected HTTP errors - Expected when users lack permissions
"401", // Unauthorized - expected when token expires
"403", // Forbidden - expected when no permissions
"404", // Not Found - expected for missing resources
],
beforeSend(event, hint) {
// Add edge runtime context for debugging
event.tags = {
...event.tags,
runtime: "edge",
};
const error = hint.originalException;
// Don't send NextAuth expected errors
if (
error &&
typeof error === "object" &&
"message" in error &&
typeof error.message === "string" &&
error.message.includes("NEXT_REDIRECT")
) {
return null;
}
return event;
},
});
+80
View File
@@ -0,0 +1,80 @@
import * as Sentry from "@sentry/nextjs";
const isProduction = process.env.SENTRY_ENVIRONMENT === "pro";
/**
* Server-side Sentry configuration
*
* This setup includes:
* - Performance monitoring for server-side operations
* - Error tracking for API routes and server actions
* - beforeSend hook to filter noise and add context
*/
Sentry.init({
// 📍 DSN - Data Source Name (identifies your Sentry project)
dsn: process.env.SENTRY_DSN,
// 🌍 Environment configuration
environment: process.env.SENTRY_ENVIRONMENT || "development",
// 📦 Release tracking
release: process.env.SENTRY_RELEASE,
// 📊 Sample Rates - Performance monitoring
// 100% in dev (test everything), 50% in production (balance visibility with costs)
tracesSampleRate: isProduction ? 0.5 : 1.0,
profilesSampleRate: isProduction ? 0.5 : 1.0,
// 🔌 Integrations
integrations: [
Sentry.extraErrorDataIntegration({
depth: 5, // Include up to 5 levels of nested objects
}),
],
// 🎣 Filter expected errors - Don't send noise to Sentry
ignoreErrors: [
// NextAuth redirect errors - Expected behavior
"NEXT_REDIRECT",
"NEXT_NOT_FOUND",
// Expected HTTP errors - Expected when users lack permissions
"401", // Unauthorized
"403", // Forbidden
"404", // Not Found
],
beforeSend(event, hint) {
// Add server context and tag errors appropriately
if (event.exception) {
const error = hint.originalException;
// Tag API errors for better filtering in Sentry dashboard
if (
error &&
typeof error === "object" &&
"message" in error &&
typeof error.message === "string"
) {
if (error.message.includes("Server error")) {
event.tags = {
...event.tags,
error_type: "server_error",
severity: "high",
};
} else if (error.message.includes("Request failed")) {
event.tags = {
...event.tags,
error_type: "api_error",
};
}
// Don't send NextAuth expected errors
if (error.message.includes("NEXT_REDIRECT")) {
return null;
}
}
}
return event;
},
});
+36
View File
@@ -0,0 +1,36 @@
/**
* Enum for standardized error types across the application
*/
export enum SentryErrorType {
// API Errors
API_ERROR = "api_error",
SERVER_ERROR = "server_error",
CLIENT_ERROR = "client_error",
// Request Processing
REQUEST_PROCESSING = "request_processing",
STREAM_PROCESSING = "stream_processing",
// Application Errors
APPLICATION_ERROR = "application_error",
UNEXPECTED_ERROR = "unexpected_error",
NON_ERROR_OBJECT = "non_error_object",
// Authentication
AUTH_ERROR = "auth_error",
PERMISSION_ERROR = "permission_error",
// Server Actions
SERVER_ACTION_ERROR = "server_action_error",
}
/**
* Enum for error sources
*/
export enum SentryErrorSource {
ERROR_BOUNDARY = "error_boundary",
API_ROUTE = "api_route",
SERVER_ACTION = "server_action",
HANDLE_API_ERROR = "handleApiError",
HANDLE_API_RESPONSE = "handleApiResponse",
}