diff --git a/.changeset/sentry-opt-in-crash-reporting.md b/.changeset/sentry-opt-in-crash-reporting.md
new file mode 100644
index 000000000..175ef0889
--- /dev/null
+++ b/.changeset/sentry-opt-in-crash-reporting.md
@@ -0,0 +1,5 @@
+---
+default: patch
+---
+
+Add opt-in Sentry crash reporting with a consent banner.
diff --git a/docs/PRIVACY.md b/docs/PRIVACY.md
index 598b3af3e..aa300dae4 100644
--- a/docs/PRIVACY.md
+++ b/docs/PRIVACY.md
@@ -85,7 +85,7 @@ Depending on the build, you can disable error reporting, enable or disable sessi
### First-time consent notice
-When a build has crash reporting configured, a notice appears the first time you open Sable. It explains that anonymous crash reports are enabled and gives you the option to opt out before any diagnostic data is sent. You can also dismiss it to keep reporting enabled.
+When a build has crash reporting configured, a notice appears the first time you open Sable. It explains that Sable can send anonymous crash reports to help fix bugs, and gives you the option to enable it. Dismissing the notice without enabling keeps crash reporting off.
This notice only appears once. Your choice is saved and can be changed at any time in **Settings → General → Diagnostics & Privacy**.
diff --git a/docs/SENTRY_INTEGRATION.md b/docs/SENTRY_INTEGRATION.md
index 5594ea165..df5511e57 100644
--- a/docs/SENTRY_INTEGRATION.md
+++ b/docs/SENTRY_INTEGRATION.md
@@ -78,7 +78,7 @@ The bug report modal (`/bugreport` command or "Bug Report" button) now includes:
- **Optional Sentry reporting**: Checkbox to send anonymous reports to Sentry
- **Debug log attachment**: Option to include recent debug logs (last 100 entries)
- **User feedback API**: Bug reports are sent as Sentry user feedback for better visibility
-- **Privacy controls**: Users can opt-out of Sentry reporting
+- **Privacy controls**: Users can opt-in to Sentry reporting
Integration points:
@@ -94,7 +94,7 @@ Comprehensive data scrubbing (full details in [SENTRY_PRIVACY.md](./SENTRY_PRIVA
- **Matrix ID anonymization**: User IDs, room IDs, and event IDs are masked
- **Session replay privacy**: All text, media, and form inputs are masked when replay is enabled
- **request header sanitization**: Authorization headers are removed
-- **User opt-out**: Users can disable Sentry entirely via settings
+- **User opt-in**: Users can enable Sentry via settings
Sensitive patterns automatically redacted:
@@ -124,14 +124,14 @@ Sentry controls are split across two settings locations:
### 6. First-Login Consent Banner
-When `VITE_SENTRY_DSN` is set and a user has never seen the crash-reporting notice (i.e. `sable_sentry_enabled` is absent from `localStorage`), a dismissible banner slides in from the bottom of the screen on first load. It explains that anonymous crash reports are enabled and links to the Privacy Policy.
+When `VITE_SENTRY_DSN` is set and a user has never seen the crash-reporting notice (i.e. `sable_sentry_enabled` is absent from `localStorage`), a dismissible banner slides in from the bottom of the screen on first load. It explains that anonymous crash reporting is available and asks if the user wants to enable it.
**Actions available in the banner:**
-| Button | Effect |
-| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
-| **Got it** / × (close) | Sets `sable_sentry_enabled = true` in `localStorage` and dismisses the banner with a fade-out animation. Reporting continues. |
-| **Opt out** | Sets `sable_sentry_enabled = false` and reloads the page. Sentry is disabled for this user going forward. |
+| Button | Effect |
+| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
+| **Enable** | Sets `sable_sentry_enabled = true` in `localStorage` and reloads the page so Sentry initialises. Reporting begins after reload. |
+| **No thanks** / × (close) | Sets `sable_sentry_enabled = false` in `localStorage` and dismisses the banner with a fade-out animation. Sentry stays disabled. |
Once the user has interacted with the banner (either action), it never appears again. The same preference can be changed later in **Settings → General → Diagnostics & Privacy**.
@@ -432,7 +432,7 @@ See [SENTRY_PRIVACY.md](./SENTRY_PRIVACY.md) for a complete, code-linked breakdo
In summary, all data sent to Sentry is:
-- **Opt-in by default** but can be disabled
+- **Off by default**: Sentry is disabled until the user explicitly opts in
- **Anonymized**: No personal data or message content
- **Filtered**: Tokens, passwords, and IDs are redacted
- **Minimal**: Only error context and debug info
diff --git a/docs/SENTRY_PRIVACY.md b/docs/SENTRY_PRIVACY.md
index 1cfbc2cae..39be916ce 100644
--- a/docs/SENTRY_PRIVACY.md
+++ b/docs/SENTRY_PRIVACY.md
@@ -9,18 +9,18 @@ configuration details see [SENTRY_INTEGRATION.md](./SENTRY_INTEGRATION.md).
## What Is Collected
Sentry is **disabled by default when no DSN is configured** and can be **opted
-out by users** at any time via Settings → General → Diagnostics & Privacy.
+in to by users** at any time via Settings → General → Diagnostics & Privacy.
### First-Login Consent Notice
When Sentry is configured, the app shows a dismissible notice the first time a
-user loads Sable. The notice explains that crash reporting is active and provides
-a one-click opt-out before any data is sent.
+user loads Sable. The notice explains that crash reporting is available and
+provides a one-click opt-in before any data is sent.
-| Action | Effect |
-| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
-| **"Got it"** or **✕ dismiss** | Preference saved as opted-in (`sable_sentry_enabled = 'true'`); notice does not appear again |
-| **"Opt out"** | Sentry disabled (`sable_sentry_enabled = 'false'`), page reloads — no Sentry data is sent for that session or any future session |
+| Action | Effect |
+| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
+| **"Enable"** | Sentry enabled (`sable_sentry_enabled = 'true'`), page reloads so Sentry initialises — data collection begins after reload |
+| **"No thanks"** or **✕ dismiss** | Preference saved as opted-out (`sable_sentry_enabled = 'false'`); notice does not appear again; no Sentry data is ever sent |
The preference persists in `localStorage` and can be changed at any time in
**Settings → General → Diagnostics & Privacy**.
diff --git a/src/app/components/telemetry-consent/TelemetryConsentBanner.test.tsx b/src/app/components/telemetry-consent/TelemetryConsentBanner.test.tsx
index 83c5fb00b..a1030889e 100644
--- a/src/app/components/telemetry-consent/TelemetryConsentBanner.test.tsx
+++ b/src/app/components/telemetry-consent/TelemetryConsentBanner.test.tsx
@@ -24,7 +24,7 @@ describe('TelemetryConsentBanner', () => {
expect(container).toBeEmptyDOMElement();
});
- it('renders nothing when the user has already acknowledged (opted in)', () => {
+ it('renders nothing when the user has already opted in', () => {
vi.stubEnv('VITE_SENTRY_DSN', TEST_DSN);
localStorage.setItem(SENTRY_KEY, 'true');
const { container } = render(