diff --git a/.env b/.env index c0364e29ae..35417637da 100644 --- a/.env +++ b/.env @@ -6,14 +6,20 @@ PROWLER_UI_VERSION="stable" AUTH_URL=http://localhost:3000 API_BASE_URL=http://prowler-api:8080/api/v1 +# deprecated, use UI_API_BASE_URL NEXT_PUBLIC_API_BASE_URL=${API_BASE_URL} +UI_API_BASE_URL=${API_BASE_URL} +# deprecated, use UI_API_DOCS_URL NEXT_PUBLIC_API_DOCS_URL=http://prowler-api:8080/api/v1/docs +UI_API_DOCS_URL=http://prowler-api:8080/api/v1/docs AUTH_TRUST_HOST=true UI_PORT=3000 # openssl rand -base64 32 AUTH_SECRET="N/c6mnaS5+SWq81+819OrzQZlmx1Vxtp/orjttJSmw8=" -# Google Tag Manager ID +# Google Tag Manager ID (empty/unset ⇒ GTM not loaded, zero egress) +# deprecated, use UI_GOOGLE_TAG_MANAGER_ID NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID="" +UI_GOOGLE_TAG_MANAGER_ID="" #### MCP Server #### PROWLER_MCP_VERSION=stable @@ -139,10 +145,16 @@ DJANGO_BROKER_VISIBILITY_TIMEOUT=86400 DJANGO_SENTRY_DSN= DJANGO_THROTTLE_TOKEN_OBTAIN=50/minute -# Sentry settings -SENTRY_ENVIRONMENT=local +# Sentry for the web app (server + browser). Empty/unset UI_SENTRY_DSN ⇒ +# Sentry disabled, zero egress. SENTRY_RELEASE (unprefixed) feeds the web app's +# server/edge SDKs. +UI_SENTRY_DSN= +UI_SENTRY_ENVIRONMENT=local SENTRY_RELEASE=local -NEXT_PUBLIC_SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT} +# Reserved runtime public config (registered now; no UI consumer yet) +# POSTHOG_KEY= +# POSTHOG_HOST= +# REO_DEV_CLIENT_ID= #### Prowler release version #### NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.31.0 diff --git a/.github/workflows/ui-container-build-push.yml b/.github/workflows/ui-container-build-push.yml index 984256af7f..3210f6e998 100644 --- a/.github/workflows/ui-container-build-push.yml +++ b/.github/workflows/ui-container-build-push.yml @@ -32,9 +32,6 @@ env: PROWLERCLOUD_DOCKERHUB_REPOSITORY: prowlercloud PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-ui - # Build args - NEXT_PUBLIC_API_BASE_URL: http://prowler-api:8080/api/v1 - permissions: {} jobs: @@ -146,7 +143,6 @@ jobs: context: ${{ env.WORKING_DIRECTORY }} build-args: | NEXT_PUBLIC_PROWLER_RELEASE_VERSION=${{ (github.event_name == 'release' || github.event_name == 'workflow_dispatch') && format('v{0}', env.RELEASE_TAG) || needs.setup.outputs.short-sha }} - NEXT_PUBLIC_API_BASE_URL=${{ env.NEXT_PUBLIC_API_BASE_URL }} push: true platforms: ${{ matrix.platform }} tags: | diff --git a/.github/workflows/ui-e2e-tests-v2.yml b/.github/workflows/ui-e2e-tests-v2.yml index ed407e890d..7165882d62 100644 --- a/.github/workflows/ui-e2e-tests-v2.yml +++ b/.github/workflows/ui-e2e-tests-v2.yml @@ -40,7 +40,8 @@ jobs: AUTH_SECRET: 'fallback-ci-secret-for-testing' AUTH_TRUST_HOST: true NEXTAUTH_URL: 'http://localhost:3000' - NEXT_PUBLIC_API_BASE_URL: 'http://localhost:8080/api/v1' + AUTH_URL: 'http://localhost:3000' + UI_API_BASE_URL: 'http://localhost:8080/api/v1' E2E_ADMIN_USER: ${{ secrets.E2E_ADMIN_USER }} E2E_ADMIN_PASSWORD: ${{ secrets.E2E_ADMIN_PASSWORD }} E2E_AWS_PROVIDER_ACCOUNT_ID: ${{ secrets.E2E_AWS_PROVIDER_ACCOUNT_ID }} @@ -165,7 +166,7 @@ jobs: timeout=150 elapsed=0 while [ $elapsed -lt $timeout ]; do - if curl -s ${NEXT_PUBLIC_API_BASE_URL}/docs >/dev/null 2>&1; then + if curl -s ${UI_API_BASE_URL}/docs >/dev/null 2>&1; then echo "Prowler API is ready!" exit 0 fi diff --git a/contrib/k8s/helm/prowler-app/templates/ui/configmap.yaml b/contrib/k8s/helm/prowler-app/templates/ui/configmap.yaml index 38d6e65ee3..856770722a 100644 --- a/contrib/k8s/helm/prowler-app/templates/ui/configmap.yaml +++ b/contrib/k8s/helm/prowler-app/templates/ui/configmap.yaml @@ -11,8 +11,7 @@ data: {{- else }} AUTH_URL: {{ .Values.ui.authUrl | quote }} {{- end }} - API_BASE_URL: "http://{{ include "prowler.fullname" . }}-api:{{ .Values.api.service.port }}/api/v1" - NEXT_PUBLIC_API_BASE_URL: "http://{{ include "prowler.fullname" . }}-api:{{ .Values.api.service.port }}/api/v1" - NEXT_PUBLIC_API_DOCS_URL: "http://{{ include "prowler.fullname" . }}-api:{{ .Values.api.service.port }}/api/v1/docs" + UI_API_BASE_URL: "http://{{ include "prowler.fullname" . }}-api:{{ .Values.api.service.port }}/api/v1" + UI_API_DOCS_URL: "http://{{ include "prowler.fullname" . }}-api:{{ .Values.api.service.port }}/api/v1/docs" AUTH_TRUST_HOST: "true" UI_PORT: {{ .Values.ui.service.port | quote }} diff --git a/contrib/k8s/helm/prowler-ui/values.yaml b/contrib/k8s/helm/prowler-ui/values.yaml index d4f2dbe137..aca0178b3a 100644 --- a/contrib/k8s/helm/prowler-ui/values.yaml +++ b/contrib/k8s/helm/prowler-ui/values.yaml @@ -21,8 +21,8 @@ fullnameOverride: "" secrets: SITE_URL: http://localhost:3000 - API_BASE_URL: http://prowler-api:8080/api/v1 - NEXT_PUBLIC_API_DOCS_URL: http://prowler-api:8080/api/v1/docs + UI_API_BASE_URL: http://prowler-api:8080/api/v1 + UI_API_DOCS_URL: http://prowler-api:8080/api/v1/docs AUTH_TRUST_HOST: True UI_PORT: 3000 # openssl rand -base64 32 diff --git a/docs/developer-guide/end2end-testing.mdx b/docs/developer-guide/end2end-testing.mdx index 0a62251531..9013a32245 100644 --- a/docs/developer-guide/end2end-testing.mdx +++ b/docs/developer-guide/end2end-testing.mdx @@ -221,9 +221,9 @@ Before running E2E tests: ``` - **Ensure Prowler API is available** - - By default, Playwright uses `NEXT_PUBLIC_API_BASE_URL=http://localhost:8080/api/v1` (configured in `playwright.config.ts`). + - By default, Playwright uses `UI_API_BASE_URL=http://localhost:8080/api/v1` (configured in `playwright.config.ts`). - Start Prowler API so it is reachable on that URL (for example, via `docker-compose-dev.yml` or the development orchestration used locally). - - If a different API URL is required, set `NEXT_PUBLIC_API_BASE_URL` accordingly before running the tests. + - If a different API URL is required, set `UI_API_BASE_URL` accordingly before running the tests. - **Ensure Prowler App UI is available** - Playwright automatically starts the Next.js server through the `webServer` block in `playwright.config.ts` (`pnpm run dev` by default). diff --git a/docs/developer-guide/environment-variables.mdx b/docs/developer-guide/environment-variables.mdx new file mode 100644 index 0000000000..1f4d17ee2e --- /dev/null +++ b/docs/developer-guide/environment-variables.mdx @@ -0,0 +1,53 @@ +--- +title: 'Environment Variable Naming Convention' +--- + +Prowler is a monorepo composed of several runtime components — Prowler App (the web user interface), Prowler API (the backend), Prowler SDK, and Prowler MCP Server (Model Context Protocol) — that frequently share a single `.env` file. To keep that shared configuration unambiguous, each component namespaces its environment variables with a component-specific prefix. + +## Component Prefixes + +Each component owns a dedicated prefix for the environment variables it reads: + +| Component | Prefix | Status | +|-----------|--------|--------| +| Prowler App (web UI) | `UI_` | Adopted | +| Prowler API (backend) | `API_` | Planned | +| Prowler SDK | `SDK_` | Planned | +| Prowler MCP Server | `MCP_` | Planned | + +## Why Component Prefixes Matter + +Component prefixes solve three concrete problems in a shared configuration file: + +- **Collisions in a shared `.env`:** Several components historically read identically named variables. The API base URL, for example, is consumed by more than one component, so a single unprefixed name is ambiguous. A component prefix removes that ambiguity. +- **Explicit ownership:** A prefix states, at a glance, which component consumes a variable. +- **Reduced accidental exposure:** For Prowler App, scoping browser-facing configuration under one intentional prefix prevents server-only values from leaking into the client bundle. + +## Prowler App + +Prowler App has adopted the `UI_` prefix. Its public configuration is resolved from the container environment at runtime rather than inlined at build time, so a single pre-built image serves any deployment. For the operational details on changing these values without rebuilding the image, see [Troubleshooting](/troubleshooting). + +The former build-time variables map to the new runtime variables as follows: + +| Former variable | New variable | +|-----------------|--------------| +| `NEXT_PUBLIC_API_BASE_URL` | `UI_API_BASE_URL` | +| `NEXT_PUBLIC_API_DOCS_URL` | `UI_API_DOCS_URL` | +| `NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID` | `UI_GOOGLE_TAG_MANAGER_ID` | +| `NEXT_PUBLIC_SENTRY_DSN`, `SENTRY_DSN` | `UI_SENTRY_DSN` | +| `NEXT_PUBLIC_SENTRY_ENVIRONMENT`, `SENTRY_ENVIRONMENT` | `UI_SENTRY_ENVIRONMENT` | + +The build-time-only Sentry variables used for source-map upload — `SENTRY_ORG`, `SENTRY_PROJECT`, `SENTRY_AUTH_TOKEN`, and `SENTRY_RELEASE` — keep their names, as they are not part of the App's runtime configuration. + +## Upcoming Breaking Change + + +Adopting the `API_`, `SDK_`, and `MCP_` prefixes for Prowler API, Prowler SDK, and Prowler MCP Server is a planned breaking change in a future release. Migrate environment configuration to the new names when upgrading. + + +Prowler API, Prowler SDK, and Prowler MCP Server have not yet adopted the convention. In a future release, the variables each of these components reads will be namespaced under `API_`, `SDK_`, and `MCP_` respectively. The per-component mapping from current to prefixed names will be documented when each change is released. + +## Deprecated Names + +- **Prowler App:** The bare server-side `SENTRY_DSN` and `SENTRY_ENVIRONMENT` are no longer read; the server and edge runtimes now read `UI_SENTRY_DSN` and `UI_SENTRY_ENVIRONMENT`. The former `NEXT_PUBLIC_*` build-time variables are deprecated but still read at runtime as a fallback when the matching `UI_*` variable is unset. This fallback will be removed in a future release, so set the `UI_*` runtime variables on the running container. +- **Prowler API, Prowler SDK, and Prowler MCP Server:** The current, unprefixed variable names are deprecated. They continue to work today and will be removed once the prefixed convention is adopted for each component, as described in [Upcoming Breaking Change](#upcoming-breaking-change). diff --git a/docs/docs.json b/docs/docs.json index 51c7e739b0..a74aa7e91d 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -417,6 +417,7 @@ "group": "Miscellaneous", "pages": [ "developer-guide/documentation", + "developer-guide/environment-variables", { "group": "Testing", "pages": [ diff --git a/docs/troubleshooting.mdx b/docs/troubleshooting.mdx index 9d60c57321..50cc43a3c0 100644 --- a/docs/troubleshooting.mdx +++ b/docs/troubleshooting.mdx @@ -201,35 +201,29 @@ When running Prowler behind a reverse proxy (nginx, Traefik, etc.) or load balan **Root Cause:** -Next.js environment variables prefixed with `NEXT_PUBLIC_` are **bundled at build time**, not runtime. The pre-built Docker images from Docker Hub (`prowlercloud/prowler-ui:stable`) are built with default internal URLs. Simply setting `NEXT_PUBLIC_API_BASE_URL` in your `.env` file or environment variables and restarting the container will **NOT** work because these values are already compiled into the JavaScript bundle. +The API base and docs URLs are resolved from the container environment **at runtime**. A single pre-built Docker image (`prowlercloud/prowler-ui:stable`) therefore serves any environment: point the URLs at your external domain and restart the container — no rebuild is required. **Solution:** -You must **rebuild** the UI Docker image with your external URL: - -```bash -# Clone the repository (if you haven't already) -git clone https://github.com/prowler-cloud/prowler.git -cd prowler/ui - -# Build with your external URL as a build argument -docker build \ - --build-arg NEXT_PUBLIC_API_BASE_URL=https://prowler.example.com/api/v1 \ - --build-arg NEXT_PUBLIC_API_DOCS_URL=https://prowler.example.com/api/v1/docs \ - -t prowler-ui-custom:latest \ - --target prod \ - . -``` - -Then update your `docker-compose.yml` to use your custom image instead of the pre-built one: +Set the runtime environment variables to your external URL and restart the UI container: ```yaml services: ui: - image: prowler-ui-custom:latest # Use your custom-built image + image: prowlercloud/prowler-ui:stable + environment: + UI_API_BASE_URL: https://prowler.example.com/api/v1 + UI_API_DOCS_URL: https://prowler.example.com/api/v1/docs # ... rest of configuration ``` +The same values can be supplied through your `.env` file: + +```bash +UI_API_BASE_URL=https://prowler.example.com/api/v1 +UI_API_DOCS_URL=https://prowler.example.com/api/v1/docs +``` + -The `NEXT_PUBLIC_` prefix is a Next.js convention that exposes environment variables to the browser. Since the browser bundle is compiled during `docker build`, these variables must be provided as build arguments, not runtime environment variables. +Earlier releases inlined these values into the JavaScript bundle at build time (via the `NEXT_PUBLIC_` prefix) and required a rebuild with `--build-arg`. That is no longer necessary: `UI_API_BASE_URL` and `UI_API_DOCS_URL` are read at container start, so updating them and restarting is sufficient. diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 2cb9f92fcb..97f0dc9830 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -28,6 +28,14 @@ All notable changes to the **Prowler UI** are documented in this file. - Threat Map no longer shows an empty map for accounts that only have Okta or Google Workspace scans [(#11542)](https://github.com/prowler-cloud/prowler/pull/11542) - Compliance attributes requests now pass the selected scan, so multi-provider universal frameworks (e.g. CSA CCM) load the check IDs of the scan's provider and Azure/GCP requirement details show their findings instead of appearing empty [(#11546)](https://github.com/prowler-cloud/prowler/pull/11546) +### 🔄 Changed + +- Public SaaS config (Sentry, Google Tag Manager, API base/docs URL) now resolves at container runtime instead of build time; self-hosted deployments set the UI config through the new `UI_`-prefixed env vars (`UI_API_BASE_URL`, `UI_API_DOCS_URL`, `UI_GOOGLE_TAG_MANAGER_ID`, `UI_SENTRY_DSN`, `UI_SENTRY_ENVIRONMENT`), with the previous `NEXT_PUBLIC_*` names still honored as a deprecated fallback [(#11500)](https://github.com/prowler-cloud/prowler/pull/11500) + +### 🐞 Fixed + +- `ui/.env` template now lists only the canonical `UI_SENTRY_DSN` and `UI_SENTRY_ENVIRONMENT` names; the deprecated `NEXT_PUBLIC_SENTRY_DSN` and `NEXT_PUBLIC_SENTRY_ENVIRONMENT` entries have been removed [(#11500)](https://github.com/prowler-cloud/prowler/pull/11500) + --- ## [1.30.0] (Prowler v5.30.0) diff --git a/ui/Dockerfile b/ui/Dockerfile index 6cf930ee5e..86673ba046 100644 --- a/ui/Dockerfile +++ b/ui/Dockerfile @@ -36,12 +36,8 @@ RUN corepack install ENV NEXT_TELEMETRY_DISABLED=1 ARG NEXT_PUBLIC_PROWLER_RELEASE_VERSION ENV NEXT_PUBLIC_PROWLER_RELEASE_VERSION=${NEXT_PUBLIC_PROWLER_RELEASE_VERSION} -ARG NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID -ENV NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID=${NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID} -ARG NEXT_PUBLIC_API_BASE_URL -ENV NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL} -ARG NEXT_PUBLIC_API_DOCS_URL -ENV NEXT_PUBLIC_API_DOCS_URL=${NEXT_PUBLIC_API_DOCS_URL} + +# GTM / API base+docs URLs are runtime container env (prod stage), not build ARGs. RUN pnpm run build @@ -78,6 +74,12 @@ EXPOSE 3000 ENV PORT=3000 ENV HOSTNAME="0.0.0.0" +# Runtime configuration is read by `node server.js` at container start and is +# NOT baked into the image. Supply it via your orchestrator (docker-compose, +# Helm/K8s): +# - required: UI_API_BASE_URL, AUTH_URL, AUTH_SECRET (missing ⇒ fail fast at boot) +# - optional: UI_API_DOCS_URL, UI_GOOGLE_TAG_MANAGER_ID, UI_SENTRY_DSN, UI_SENTRY_ENVIRONMENT +# - reserved: POSTHOG_KEY, POSTHOG_HOST, REO_DEV_CLIENT_ID (no consumer yet) # server.js is created by next build from the standalone output # https://nextjs.org/docs/pages/api-reference/next-config-js/output CMD ["node", "server.js"] diff --git a/ui/__tests__/msw/handlers/attack-paths.ts b/ui/__tests__/msw/handlers/attack-paths.ts index 39076666dd..7c76fa574b 100644 --- a/ui/__tests__/msw/handlers/attack-paths.ts +++ b/ui/__tests__/msw/handlers/attack-paths.ts @@ -10,7 +10,7 @@ import type { QueryResultAttributes, } from "@/types/attack-paths"; -const API = process.env.NEXT_PUBLIC_API_BASE_URL; +const API = process.env.UI_API_BASE_URL; type JsonApiErrorBody = { errors: Array<{ detail: string; status: string }>; diff --git a/ui/app/(auth)/alerts/confirm/confirm-alert-recipient.test.ts b/ui/app/(auth)/alerts/confirm/confirm-alert-recipient.test.ts index d95966e680..41118bd6e2 100644 --- a/ui/app/(auth)/alerts/confirm/confirm-alert-recipient.test.ts +++ b/ui/app/(auth)/alerts/confirm/confirm-alert-recipient.test.ts @@ -14,7 +14,7 @@ const lastFetchCall = (): { url: string; init: RequestInit } => { describe("confirmAlertRecipient", () => { beforeEach(() => { vi.stubGlobal("fetch", fetchMock); - vi.stubEnv("NEXT_PUBLIC_API_BASE_URL", "https://api.example.com/api/v1"); + vi.stubEnv("UI_API_BASE_URL", "https://api.example.com/api/v1"); fetchMock.mockResolvedValue( new Response( JSON.stringify({ @@ -104,7 +104,8 @@ describe("confirmAlertRecipient", () => { }); it("returns the fallback message when the API base URL is missing", async () => { - // Given + // Given - neither the new name nor its legacy fallback is set + vi.stubEnv("UI_API_BASE_URL", ""); vi.stubEnv("NEXT_PUBLIC_API_BASE_URL", ""); // When @@ -120,6 +121,21 @@ describe("confirmAlertRecipient", () => { expect(fetchMock).not.toHaveBeenCalled(); }); + it("falls back to the deprecated NEXT_PUBLIC_API_BASE_URL when UI_API_BASE_URL is unset", async () => { + // Given - only the legacy name is configured + vi.stubEnv("UI_API_BASE_URL", undefined); + vi.stubEnv("NEXT_PUBLIC_API_BASE_URL", "https://legacy.example.com/api/v1"); + + // When + const result = await confirmAlertRecipient("token-1"); + + // Then + expect(result.ok).toBe(true); + expect(lastFetchCall().url).toBe( + "https://legacy.example.com/api/v1/alerts/recipients/confirm?token=token-1", + ); + }); + it("returns the fallback message when the request fails", async () => { // Given fetchMock.mockRejectedValueOnce(new Error("network down")); diff --git a/ui/app/(auth)/alerts/confirm/confirm-alert-recipient.ts b/ui/app/(auth)/alerts/confirm/confirm-alert-recipient.ts index b567bb4124..19e9216339 100644 --- a/ui/app/(auth)/alerts/confirm/confirm-alert-recipient.ts +++ b/ui/app/(auth)/alerts/confirm/confirm-alert-recipient.ts @@ -1,3 +1,5 @@ +import { readEnv } from "@/lib/runtime-env"; + interface AlertConfirmApiResponse { state?: string; message?: string; @@ -41,7 +43,7 @@ const toState = (payload: unknown): string => { export const confirmAlertRecipient = async ( token?: string, ): Promise => { - const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL; + const apiBaseUrl = readEnv("UI_API_BASE_URL", "NEXT_PUBLIC_API_BASE_URL"); if (!apiBaseUrl) { return { ok: false, diff --git a/ui/app/(auth)/alerts/unsubscribe/unsubscribe-alert-recipient.test.ts b/ui/app/(auth)/alerts/unsubscribe/unsubscribe-alert-recipient.test.ts index 8a595fa63e..508625b4be 100644 --- a/ui/app/(auth)/alerts/unsubscribe/unsubscribe-alert-recipient.test.ts +++ b/ui/app/(auth)/alerts/unsubscribe/unsubscribe-alert-recipient.test.ts @@ -14,7 +14,7 @@ const lastFetchCall = (): { url: string; init: RequestInit } => { describe("unsubscribeAlertRecipient", () => { beforeEach(() => { vi.stubGlobal("fetch", fetchMock); - vi.stubEnv("NEXT_PUBLIC_API_BASE_URL", "https://api.example.com/api/v1"); + vi.stubEnv("UI_API_BASE_URL", "https://api.example.com/api/v1"); fetchMock.mockResolvedValue( new Response( JSON.stringify({ @@ -102,4 +102,37 @@ describe("unsubscribeAlertRecipient", () => { "https://api.example.com/api/v1/alerts/recipients/unsubscribe", ); }); + + it("returns the fallback message when the API base URL is missing", async () => { + // Given - neither the new name nor its legacy fallback is set + vi.stubEnv("UI_API_BASE_URL", ""); + vi.stubEnv("NEXT_PUBLIC_API_BASE_URL", ""); + + // When + const result = await unsubscribeAlertRecipient("token-1"); + + // Then + expect(result).toEqual({ + ok: false, + state: "missing_api_base_url", + message: + "We could not process this unsubscribe link. Please try again later.", + }); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("falls back to the deprecated NEXT_PUBLIC_API_BASE_URL when UI_API_BASE_URL is unset", async () => { + // Given - only the legacy name is configured + vi.stubEnv("UI_API_BASE_URL", undefined); + vi.stubEnv("NEXT_PUBLIC_API_BASE_URL", "https://legacy.example.com/api/v1"); + + // When + const result = await unsubscribeAlertRecipient("token-1"); + + // Then + expect(result.ok).toBe(true); + expect(lastFetchCall().url).toBe( + "https://legacy.example.com/api/v1/alerts/recipients/unsubscribe?token=token-1", + ); + }); }); diff --git a/ui/app/(auth)/alerts/unsubscribe/unsubscribe-alert-recipient.ts b/ui/app/(auth)/alerts/unsubscribe/unsubscribe-alert-recipient.ts index af64165a1e..8c3a4e2a79 100644 --- a/ui/app/(auth)/alerts/unsubscribe/unsubscribe-alert-recipient.ts +++ b/ui/app/(auth)/alerts/unsubscribe/unsubscribe-alert-recipient.ts @@ -1,3 +1,5 @@ +import { readEnv } from "@/lib/runtime-env"; + interface AlertUnsubscribeApiResponse { state?: string; message?: string; @@ -41,7 +43,7 @@ const toState = (payload: unknown): string => { export const unsubscribeAlertRecipient = async ( token?: string, ): Promise => { - const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL; + const apiBaseUrl = readEnv("UI_API_BASE_URL", "NEXT_PUBLIC_API_BASE_URL"); if (!apiBaseUrl) { return { ok: false, diff --git a/ui/app/(auth)/layout.tsx b/ui/app/(auth)/layout.tsx index 07fe3a60c3..79a4ce4e89 100644 --- a/ui/app/(auth)/layout.tsx +++ b/ui/app/(auth)/layout.tsx @@ -2,12 +2,15 @@ import "@/styles/globals.css"; import { GoogleTagManager } from "@next/third-parties/google"; import { Metadata, Viewport } from "next"; +import { connection } from "next/server"; import { ReactNode, Suspense } from "react"; +import { RuntimePublicConfig } from "@/components/runtime-config/runtime-public-config"; import { NavigationProgress, Toaster } from "@/components/ui"; import { fontSans } from "@/config/fonts"; import { siteConfig } from "@/config/site"; import { cn } from "@/lib"; +import { readEnv } from "@/lib/runtime-env"; import { Providers } from "../providers"; @@ -29,10 +32,27 @@ export const viewport: Viewport = { ], }; -export default function AuthLayout({ children }: { children: ReactNode }) { +export default async function AuthLayout({ + children, +}: { + children: ReactNode; +}) { + // Force dynamic rendering so the read below resolves from the container env + // at request time rather than being snapshotted at build (independent of the + // island's own connection() call). + await connection(); + + // Server-side runtime read. Empty/unset id ⇒ GoogleTagManager is not mounted + const gtmId = readEnv( + "UI_GOOGLE_TAG_MANAGER_ID", + "NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID", + ); + return ( - + + + {children} - + {gtmId && } diff --git a/ui/app/(prowler)/layout.tsx b/ui/app/(prowler)/layout.tsx index a093232e1c..2554ab02b9 100644 --- a/ui/app/(prowler)/layout.tsx +++ b/ui/app/(prowler)/layout.tsx @@ -11,6 +11,7 @@ import { OnboardingGate, OnboardingSequenceBanner, } from "@/components/onboarding"; +import { RuntimePublicConfig } from "@/components/runtime-config/runtime-public-config"; import MainLayout from "@/components/ui/main-layout/main-layout"; import { NavigationProgress } from "@/components/ui/navigation-progress"; import { Toaster } from "@/components/ui/toast"; @@ -76,7 +77,9 @@ export default async function RootLayout({ return ( - + + + 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", - }); - } -} diff --git a/ui/app/providers.tsx b/ui/app/providers.tsx index 7bda5d45fe..41157df06c 100644 --- a/ui/app/providers.tsx +++ b/ui/app/providers.tsx @@ -1,8 +1,5 @@ "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"; diff --git a/ui/components/integrations/saml/saml-config-form.tsx b/ui/components/integrations/saml/saml-config-form.tsx index a995fffceb..935f5412e9 100644 --- a/ui/components/integrations/saml/saml-config-form.tsx +++ b/ui/components/integrations/saml/saml-config-form.tsx @@ -18,7 +18,7 @@ import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet"; import { CustomServerInput } from "@/components/ui/custom"; import { CustomLink } from "@/components/ui/custom/custom-link"; import { FormButtons } from "@/components/ui/form"; -import { apiBaseUrl } from "@/lib"; +import { useRuntimeConfig } from "@/hooks/use-runtime-config"; const validateXMLContent = ( xmlContent: string, @@ -253,10 +253,12 @@ export const SamlConfigForm = ({ reader.readAsText(file); }; + const { apiBaseUrl } = useRuntimeConfig(); const trimmedEmailDomain = emailDomain.trim(); - const acsUrl = trimmedEmailDomain - ? `${apiBaseUrl}/accounts/saml/${trimmedEmailDomain}/acs/` - : ""; + const acsUrl = + trimmedEmailDomain && apiBaseUrl + ? `${apiBaseUrl}/accounts/saml/${trimmedEmailDomain}/acs/` + : ""; return (
before +// the client bundle, so module-load consumers (Sentry init) read it race-free. +export async function RuntimePublicConfig() { + const config = await getRuntimePublicConfig(); + + return ( + breakout so the script tag is not terminated early", () => { + // Given + const value = { sentryDsn: "" }; + + // When + const serialized = serializeForScript(value); + + // Then + expect(serialized).not.toContain(""); + expect(serialized).not.toContain("<"); + expect(serialized).not.toContain(">"); + }); + + it("neutralizes the HTML comment opener " }; + + // When + const serialized = serializeForScript(value); + + // Then + expect(serialized).not.toContain("