This document describes the Sentry error tracking and monitoring integration added to Sable. For a detailed breakdown of what data is collected and how it is protected, see SENTRY_PRIVACY.md.
Sentry is integrated with Sable to provide:
- Error tracking: Automatic capture and reporting of errors and exceptions
- Performance monitoring: Track application performance and identify bottlenecks
- User feedback: Collect bug reports with context from users
- Session replay: Record user sessions (with privacy controls) for debugging
- Breadcrumbs: Track user actions leading up to errors
- Debug log integration: Attach internal debug logs to error reports
Two non-Sentry bugs were found and fixed in the course of building this integration:
Problem: When a room with a single cached event (list subscription, timeline_limit=1) becomes
fully subscribed and the SDK delivers N new events, the TimelineReset fires before any events land
on the fresh timeline. The "stay at bottom" effect queues a scrollToBottom while the DOM is still
empty (range end=0). By the time real events load, the scroll has already fired against an empty
container and is a no-op — the user's view stalls mid-list.
Fix: The stay-at-bottom useEffect now increments scrollToBottomRef.current.count after
calling setTimeline(getInitialTimeline(room)), re-queuing the scroll for after the first batch of
events arrives and the DOM has content.
File: src/app/features/room/RoomTimeline.tsx
Problem: A phase !== undefined guard was always evaluating to true because the TypeScript
type for phase had no undefined branch at that point in the control flow.
Fix: Removed the dead branch. TypeScript no longer emits a TS2367 comparison error here.
File: src/app/hooks/useCallSignaling.ts
All errors are automatically captured and sent to Sentry with:
- Stack traces
- User context (anonymized)
- Device and browser information
- Recent breadcrumbs (user actions)
- Debug logs (when enabled)
The internal debug logger now integrates with Sentry:
- Breadcrumbs: All debug logs are added as breadcrumbs for context
- Error capture: Errors logged to the debug logger are automatically sent to Sentry
- Warning sampling: 10% of warnings are sent to Sentry to avoid overwhelming the system
- Log attachment: Recent logs can be attached to bug reports for additional context
Key integration points:
src/app/utils/debugLogger.ts- Enhanced with Sentry breadcrumb and error capture- Automatic breadcrumb creation for all log entries
- Error objects in log data are captured as exceptions
- 10% sampling rate for warnings to control volume
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-in to Sentry reporting
Integration points:
src/app/features/bug-report/BugReportModal.tsx- Added Sentry options and submission logic- Automatically attaches platform info, version, and user agent
- Links bug reports to Sentry events for tracking
Comprehensive data scrubbing (full details in SENTRY_PRIVACY.md):
- Token masking: All access tokens, passwords, and authentication data are redacted
- 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-in: Users can enable Sentry via settings
Sensitive patterns automatically redacted:
access_token,password,token,refresh_tokensession_id,sync_token,next_batch- Matrix user IDs (
@user:server) - Matrix room IDs (
!room:server) - Matrix event IDs (
$event_id)
Sentry controls are split across two settings locations:
Settings → General → Diagnostics & Privacy (user-facing):
- Enable/disable error reporting: Toggle Sentry error tracking on/off
- Session replay control: Enable/disable session recording (opt-in)
- Link to the privacy policy
Settings → Developer Tools → Error Tracking (Sentry) (power-user):
- Breadcrumb categories: Granular control over which log categories are sent as breadcrumbs
- Session stats: Live error/warning counts for the current page load
- Export debug logs: Download the in-memory log buffer as JSON for offline analysis
- Attach debug logs: Manually attach recent logs to next error report
- Test buttons: Force an error, test feedback, test message capture
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 |
|---|---|
| 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.
Implementation: src/app/components/telemetry-consent/TelemetryConsentBanner.tsx — rendered inside the logged-in client layout so it only appears after a session is established.
Self-hosters: If you do not set
VITE_SENTRY_DSN, the banner is never shown and Sentry is entirely disabled at build time. No network requests are made to Sentry.
Configure Sentry via environment variables:
# Required: Your Sentry DSN (if not set, Sentry is disabled)
VITE_SENTRY_DSN=https://[email protected]/project-id
# Required: Environment name - controls sampling rates
# - "production" = 10% trace/replay sampling (cost-effective for production)
# - "preview" = 100% trace/replay sampling (full debugging for PR previews)
# - "development" = 100% trace/replay sampling (full debugging for local dev)
VITE_SENTRY_ENVIRONMENT=production
# Optional: Release version for tracking (defaults to VITE_APP_VERSION)
VITE_SENTRY_RELEASE=1.7.0
# Optional: For uploading source maps to Sentry (CI/CD only)
SENTRY_AUTH_TOKEN=your-sentry-auth-token
SENTRY_ORG=your-org-slug
SENTRY_PROJECT=your-project-slugSable is compiled at build time, so VITE_* variables must be passed as Docker
build arguments — they cannot be injected at container runtime via a plain
docker run -e flag. The easiest way for self-hosters to supply them is with
a .env file and docker-compose.
# .env — never commit this file
VITE_SENTRY_DSN=https://[email protected]/XXXXXXX
VITE_SENTRY_ENVIRONMENT=productionThe VITE_SENTRY_ENVIRONMENT value controls sampling rates (see table below).
Leave it as production for a live deployment.
The args block forwards the variables from .env into the Docker build
stage so Vite can embed them in the bundle:
services:
sable:
build:
context: .
args:
- VITE_SENTRY_DSN=${VITE_SENTRY_DSN}
- VITE_SENTRY_ENVIRONMENT=${VITE_SENTRY_ENVIRONMENT}
ports:
- '8080:8080'Then build and start with:
docker compose --env-file .env up --buildOpen the browser console after loading your instance — you should see:
[Sentry] Initialized for production environment
[Sentry] DSN configured: https://your-key@o...
If you see [Sentry] Disabled - no DSN provided, the build arg was not
picked up — double-check the args block and that your .env file is in the
same directory as docker-compose.yml.
If you use plain docker build, pass build args directly:
docker build \
--build-arg VITE_SENTRY_DSN="https://[email protected]/XXXXXXX" \
--build-arg VITE_SENTRY_ENVIRONMENT="production" \
-t sable .Security note: DSN values embedded in the JavaScript bundle are visible to any user who opens DevTools. This is normal and expected for Sentry DSNs — they are designed to be public-facing ingest keys. Rate-limiting and origin restrictions on the Sentry project side are the correct controls.
Production deployment (from dev branch):
- Set
VITE_SENTRY_ENVIRONMENT=production - Gets 10% sampling for traces and session replay
- Cost-effective for production usage
- Configured in
.github/workflows/cloudflare-web-deploy.yml
Preview deployments (PR previews, Cloudflare Pages):
- Set
VITE_SENTRY_ENVIRONMENT=preview - Gets 100% sampling for traces and session replay
- Full debugging capabilities for testing
- Configured in
.github/workflows/cloudflare-web-preview.yml
Local development:
VITE_SENTRY_ENVIRONMENTnot set (defaults todevelopmentvia Vite MODE)- Gets 100% sampling for traces and session replay
- Full debugging capabilities
Sampling rates by environment:
Environment | Traces | Profiles | Session Replay | Error Replay
---------------|--------|----------|----------------|-------------
production | 10% | 10% | 10% | 100%
preview | 100% | 100% | 100% | 100%
development | 100% | 100% | 100% | 100%
Browser profiling requires a
Document-Policy: js-profilingresponse header on your HTML document. This is already included in the providedCaddyfileand nginx config. For other servers, add the header to the response servingindex.html.
Users can control Sentry via localStorage:
// Disable Sentry entirely (requires page refresh)
localStorage.setItem('sable_sentry_enabled', 'false');
// Disable session replay only (requires page refresh)
localStorage.setItem('sable_sentry_replay_enabled', 'false');Or use the UI in Settings → General → Diagnostics & Privacy.
Beyond automatic error capture, Sable has hand-crafted monitoring at key lifecycle points. See SENTRY_PRIVACY.md for the full metrics reference. Key areas:
| Area | What's tracked |
|---|---|
| Auth | Login failures (by errcode), forced server logouts |
| Sync | Transport type, degraded states, cycle stats, initial sync latency, time-to-ready, total rooms loaded, active subscriptions |
| Cryptography | Decryption failures (by failure reason), key backup errors, store wipes, E2E verification outcomes, bulk decryption latency |
| Messaging | Send latency, send errors, local-echo NOT_SENT events |
| Timeline | Opens, virtual window size, jump-load latency, re-initialisations, limited sync resets, scroll offset at load, pagination errors |
| Pagination | Pagination latency (sable.pagination.latency_ms) and errors per direction |
| Sliding sync | Room subscription latency (sable.sync.room_sub_latency_ms), events per subscription batch (sable.sync.room_sub_event_count) |
| Scroll / UX | atBottom transitions with rapid-flip anomaly detection, scroll-to-bottom trigger warnings when user is scrolled up |
| Calls | sable.call.start.attempt/error, sable.call.answered, sable.call.declined, active/ended/timeout counters |
| Message actions | sable.message.delete.*, sable.message.forward.*, sable.message.report.*, sable.message.reaction.toggle |
| Media | Upload latency, upload size, cache stats |
| Background clients | Per-account notification client count, startup failures |
Fatal errors that are caught by useAsyncCallback state (and therefore never
reach React's ErrorBoundary) are explicitly forwarded with captureException:
- Client load failure (
phase: load) - Client start failure (
phase: start) - Background notification client startup failure
All hand-crafted breadcrumbs use structured Sentry categories that appear in the Sentry issue timeline and can be filtered in the developer settings panel.
| Category | Where emitted | What it records |
|---|---|---|
auth |
ClientRoot.tsx |
Login session start, forced logout |
sync |
initMatrix.ts, SyncStatus.tsx |
Sync state transitions, degraded states, client ready |
sync.sliding |
slidingSync.ts |
First room subscription data: latency, event count |
timeline.sync |
RoomTimeline.tsx |
SDK-initiated TimelineReset (limited sync gap) — fires before events arrive |
timeline.events |
RoomTimeline.tsx |
Every eventsLength batch: delta, batch size label, range gap, atBottom |
ui.scroll |
RoomTimeline.tsx |
atBottom true→false transitions, rapid-flip warnings, scroll-to-bottom fires |
ui.timeline |
RoomTimeline.tsx |
Virtual paginator window shifts (range start/end changes) |
call.signal |
useCallSignaling.ts, IncomingCallModal.tsx |
Call signal state changes, answer/decline |
crypto |
useKeyBackup.ts |
Key backup errors |
media |
ClientNonUIFeatures.tsx |
Blob cache stats on blob URL creation |
-
src/instrument.ts- Enhanced Sentry initialization with privacy controls
- Added user preference checks
- Improved data scrubbing for Matrix-specific data
- Conditional session replay based on user settings
-
src/app/utils/debugLogger.ts- Added Sentry import
- New
sendToSentry()method for breadcrumbs and error capture - New
exportLogsForSentry()method - New
attachLogsToSentry()method - Integrated into main
log()method
-
src/app/features/bug-report/BugReportModal.tsx- Added Sentry and debug logger imports
- New state for Sentry options (
sendToSentry,includeDebugLogs) - Enhanced
handleSubmit()with Sentry user feedback - New UI checkboxes for Sentry options
-
src/app/features/settings/developer-tools/SentrySettings.tsx(new file)- New settings panel component
- Controls for Sentry and session replay
- Manual log attachment
-
src/app/features/settings/developer-tools/DevelopTools.tsx- Added SentrySettings import and component
- Tracing sample rate: 100% in development, 10% in production
- Session replay sample rate: 10% of all sessions, 100% of error sessions
- Warning capture rate: 10% to avoid overwhelming Sentry
- Breadcrumb retention: All breadcrumbs retained for context
- Log attachment limit: Last 100 debug log entries
- Breadcrumbs are added synchronously but are low-overhead
- Error capture is asynchronous and non-blocking
- Warning sampling (10%) prevents excessive Sentry usage
- Session replay only captures when enabled by user
- Debug log attachment limited to most recent entries
import { getDebugLogger } from '$utils/debugLogger';
// Errors are automatically sent to Sentry
const logger = createDebugLogger('myNamespace');
logger.error('sync', 'Sync failed', error); // Sent to Sentry
// Manually attach logs before capturing an error
const debugLogger = getDebugLogger();
debugLogger.attachLogsToSentry(100);
Sentry.captureException(error);-
Report a bug with Sentry:
- Type
/bugreportor click "Bug Report" button - Fill in the form
- Check "Send anonymous report to Sentry"
- Check "Include recent debug logs" for more context
- Submit
- Type
-
Disable Sentry:
- Go to Settings → Developer Tools
- Enable Developer Tools
- Scroll to "Error Tracking (Sentry)"
- Toggle off "Enable Sentry Error Tracking"
- Refresh the page
- Better bug tracking and faster fixes
- Optional participation with privacy controls
- Transparent data usage
- Real-time error notifications
- Rich context with breadcrumbs and logs
- Performance monitoring
- User feedback integrated with errors
- Replay sessions to reproduce bugs
See SENTRY_PRIVACY.md for a complete, code-linked breakdown of what is collected, what is masked, and how user controls work.
In summary, all data sent to Sentry is:
- 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
- Transparent: Users can see what's being sent
No message content, room conversations, or personal information is ever sent to Sentry.
To test the integration:
-
Test error reporting:
- Go to Settings → General → Diagnostics & Privacy
- Check that Sentry is enabled and
VITE_SENTRY_DSNis set - Open the browser console and run:
window.Sentry?.captureMessage('Test message') - Check the Sentry dashboard for the event
-
Test bug report integration:
- Type
/bugreport - Fill in form with test data
- Enable "Send anonymous report to Sentry"
- Submit and check Sentry
- Type
-
Test privacy controls:
- Disable Sentry in settings
- Refresh page
- Trigger an error (should not appear in Sentry)
- Re-enable and verify errors are captured again
- Check that
VITE_SENTRY_DSNis set - Check that Sentry is enabled in settings
- Check browser console for Sentry initialization message
- Verify network requests to Sentry are not blocked
- Check
beforeSendhook ininstrument.ts - Add new patterns to the scrubbing regex
- Test with actual data to verify masking
- Reduce tracing sample rate in production
- Disable session replay if not needed
- Monitor Sentry quota usage
- Adjust warning sampling rate