mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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" };
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Generated
+3045
-397
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,2 @@
|
||||
// Re-export all Sentry utilities
|
||||
export * from "./utils";
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -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",
|
||||
}
|
||||
Reference in New Issue
Block a user