diff --git a/docker-compose.yml b/docker-compose.yml index f678ae1a..f5007fc2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -561,6 +561,9 @@ services: # captcha is off server-side. Set WARMBLY_TURNSTILE_KEY to your site key # when you set CAPTCHA_PROVIDER=turnstile. WARMBLY_TURNSTILE_KEY: ${WARMBLY_TURNSTILE_KEY:-1x00000000000000000000AA} + # Unset means the dashboard initialises no error reporting and contacts + # no Sentry host. Point it at your own project to collect browser errors. + WARMBLY_SENTRY_DSN: ${WARMBLY_SENTRY_DSN:-} depends_on: backend: { condition: service_healthy } diff --git a/docs/content/docs/development/configuration.mdx b/docs/content/docs/development/configuration.mdx index a30fa2f8..0b8d4841 100644 --- a/docs/content/docs/development/configuration.mdx +++ b/docs/content/docs/development/configuration.mdx @@ -380,8 +380,11 @@ Delayed sends run through the local poller, so the backend must be running for s | Variable | What it does | Default | |---|---|---| | `SENTRY_DSN` | Error reporting. Optional in every environment, including `prod` | unset | +| `WARMBLY_SENTRY_DSN` | Browser error reporting for the dashboard container, read at container start like the other `WARMBLY_*` values. Unset means the dashboard initialises no reporting SDK and contacts no Sentry host | unset | | `APNS_KEY` or `APNS_KEY_PATH`, `APNS_KEY_ID`, `APNS_TEAM_ID`, `APNS_TOPIC` | Mobile push on backend and consumer. Partial configuration disables push with a warning, never a crash | unset | +Error reporting is off by default everywhere. A DSN is the operator's choice, and each of these is read by one process, so pointing the backend at a project does not make the dashboard report too. Both accept Sentry Cloud, a self-hosted Sentry or any Sentry-compatible server. + ## Updates | Variable | What it does | Default | Restart needed | diff --git a/docs/content/docs/development/data-control.mdx b/docs/content/docs/development/data-control.mdx index c506cb8b..7d1811eb 100644 --- a/docs/content/docs/development/data-control.mdx +++ b/docs/content/docs/development/data-control.mdx @@ -206,6 +206,18 @@ A self-hosted Warmbly makes no outbound call of its own except one, and it is of Everything else is you: mail through the mailboxes you connect, DNS lookups for the domains you check, and whatever integrations you configure. There is no telemetry, no phone-home, and no license check. +### Error reporting + +An instance reports errors nowhere unless you point it somewhere. Every service reads its own DSN and none of them ship with one, so a default install sends no crash, no stack trace and no browser error to anybody, including us. + +| Service | Variable | +|---|---| +| Backend, consumer, worker | `SENTRY_DSN` | +| Dashboard container | `WARMBLY_SENTRY_DSN` | +| Realtime | `SENTRY_DSN` | + +Set one and that service reports to whatever Sentry Cloud project, self-hosted Sentry or Sentry-compatible server you name. Leave it unset, which is the default the installer writes, and the SDK is never initialised: there is no host to contact and nothing to opt out of. + ## See also - [Install](/development/install/): the wizard that asks all of this up front diff --git a/web/docker-entrypoint.sh b/web/docker-entrypoint.sh index 9b0a6e7c..0a6d5b48 100644 --- a/web/docker-entrypoint.sh +++ b/web/docker-entrypoint.sh @@ -7,7 +7,8 @@ cat > /usr/share/nginx/html/config.js < { setPasskeyStatus("error"); - Sentry.captureException(e); + captureException(e); }) .finally(() => { explicitPasskeyChallengePendingRef.current = false; diff --git a/web/src/lib/information.ts b/web/src/lib/information.ts index bb0ddb72..3d038cff 100644 --- a/web/src/lib/information.ts +++ b/web/src/lib/information.ts @@ -9,6 +9,8 @@ export const API_URL = runtimeEnv("API_URL", import.meta.env.VITE_API_URL); // (no path), so this is the single place the /v1 prefix is applied. export const API_BASE_URL = `${API_URL}/v1`; export const TURNSTILE_KEY = runtimeEnv("TURNSTILE_KEY", import.meta.env.VITE_TURNSTILE_KEY); +// Empty means browser error reporting is never initialised. See lib/observability. +export const SENTRY_DSN = runtimeEnv("SENTRY_DSN", import.meta.env.VITE_SENTRY_DSN); export const HUMAN_VERIFICATION_FAIL = "We couldn’t verify you’re human. Please try the security check again or reload the page."; export const PASSWORD_FAIL = "The password must be at least 8 characters long and contain both uppercase and lowercase letters, as well as a number." export const TOKEN_KEY = "auth_token"; diff --git a/web/src/lib/observability.ts b/web/src/lib/observability.ts new file mode 100644 index 00000000..89bfa3bb --- /dev/null +++ b/web/src/lib/observability.ts @@ -0,0 +1,36 @@ +// Browser error reporting. +// +// The DSN is the operator's choice, not a requirement of the software: the +// dashboard image is the same for the hosted service and for a self-host, so a +// literal DSN in the bundle would make every self-hosted install report its +// users' errors, URLs and IPs to somebody else's Sentry. It comes from the +// container-injected runtime config instead, and an unset DSN means the SDK is +// never initialised, so nothing is ever sent anywhere. +// +// The SDK is imported statically rather than lazily so that its global handlers +// are installed before the first render: a broken deploy fails during boot, and +// a chunk still in flight would miss exactly that error. Not initialising it +// costs a self-hoster some dead bundle weight and zero network calls. +import * as Sentry from "@sentry/react"; +import { SENTRY_DSN } from "./information"; + +let reporting = false; + +// initErrorReporting is called once, before the app renders. +export function initErrorReporting(): void { + if (!SENTRY_DSN) return; + + Sentry.init({ + dsn: SENTRY_DSN, + sendDefaultPii: true, + environment: import.meta.env.MODE, + }); + reporting = true; +} + +// captureException reports an error the app handled itself. A no-op when no DSN +// is configured. +export function captureException(error: unknown): void { + if (!reporting) return; + Sentry.captureException(error); +} diff --git a/web/src/main.tsx b/web/src/main.tsx index 649727b3..142dab03 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -65,13 +65,9 @@ import NotFound from './app/not-found'; import { Toaster } from '@/components/ui/toaster'; -import * as Sentry from "@sentry/react"; +import { initErrorReporting } from "@/lib/observability"; -Sentry.init({ - dsn: "https://412466daced4b1d85ee040eef66efc95@o4510248538472448.ingest.us.sentry.io/4510248563113984", - sendDefaultPii: true, - environment: import.meta.env.MODE -}) +initErrorReporting(); import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { ReactQueryDevtools } from "@tanstack/react-query-devtools"