Files
prowler/ui/instrumentation-client.ts
T
Pablo Fernandez Guerra (PFE) 853610bbbf feat(ui): resolve public SaaS config at container runtime (#11500)
Co-authored-by: Pablo F.G <pablo.fernandez@prowler.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 15:12:18 +02:00

173 lines
5.4 KiB
TypeScript

/**
* Next.js Client Instrumentation
*
* This file runs on the client BEFORE React hydration. It is responsible for:
* - Initializing browser Sentry from the runtime data island, so a single
* prebuilt image enables/disables Sentry per deployment via `UI_SENTRY_DSN`
* (unset DSN ⇒ `Sentry.init` is never called ⇒ zero egress).
* - Driving the navigation progress bar on router transitions.
*
* Running here (rather than in a React effect) means Sentry is configured
* before hydration, so browserTracing's performance observers attach early and
* the error boundary in `global-error.tsx` is covered.
*
* @see https://nextjs.org/docs/app/api-reference/file-conventions/instrumentation-client
*/
import * as Sentry from "@sentry/nextjs";
import {
cancelProgress,
startProgress,
} from "@/components/ui/navigation-progress/use-navigation-progress";
import { getRuntimeConfigClient } from "@/lib/get-runtime-config.client";
export const NAVIGATION_TYPE = {
PUSH: "push",
REPLACE: "replace",
TRAVERSE: "traverse",
} as const;
type NavigationType = (typeof NAVIGATION_TYPE)[keyof typeof NAVIGATION_TYPE];
const { sentryDsn, sentryEnvironment } = getRuntimeConfigClient();
// Only initialize Sentry in the browser when a runtime DSN is configured.
if (typeof window !== "undefined" && sentryDsn) {
// Default to a non-dev environment so an unset UI_SENTRY_ENVIRONMENT never
// runs the browser SDK in dev mode (debug logging, 100% sampling, synthetic
// dev user); only an explicit "local" enables it.
const environment = sentryEnvironment ?? "production";
const isDevelopment = environment === "local";
/**
* 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 - resolved at runtime from the data island
dsn: sentryDsn,
// 🌍 Environment - separate dev errors from production
environment,
// 📦 Release - the browser can only read the build-time public release
// version; SENTRY_RELEASE is non-public and never reaches the client. The
// server/edge SDKs read SENTRY_RELEASE for parity (D10). No new release var.
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 - browserTracingIntegration is client-only
integrations: [
// 📊 Performance Monitoring: Core Web Vitals + RUM
// Tracks LCP, FID, CLS, INP
Sentry.browserTracingIntegration({
enableLongTask: true, // Detect tasks that block UI (>50ms)
enableInp: true, // Interaction to Next Paint (Core Web Vital)
}),
],
// 🎣 Filter expected errors / noise 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",
});
}
}
function getCurrentUrl(): string {
return window.location.pathname + window.location.search;
}
/**
* Called by Next.js when router navigation begins.
*
* Triggers the navigation progress bar AND forwards the transition to Sentry
* (`captureRouterTransitionStart` is a safe no-op when Sentry is
* uninitialized).
*/
export function onRouterTransitionStart(
url: string,
navigationType: NavigationType,
) {
const currentUrl = getCurrentUrl();
if (url === currentUrl) {
// Same URL - cancel any ongoing progress
cancelProgress();
} else {
// Different URL - start progress
startProgress();
}
Sentry.captureRouterTransitionStart(url, navigationType);
}