Skip to content

feat(sentry): error tracking, performance monitoring, and session replay#280

Open
Just-Insane wants to merge 27 commits intodevfrom
feat/sentry-pr
Open

feat(sentry): error tracking, performance monitoring, and session replay#280
Just-Insane wants to merge 27 commits intodevfrom
feat/sentry-pr

Conversation

@Just-Insane
Copy link
Contributor

@Just-Insane Just-Insane commented Mar 15, 2026

Adds comprehensive Sentry error tracking, performance monitoring, and session replay to Sable.

Commits

1. feat(sentry): initialize Sentry SDK with environment-based sampling
Adds @sentry/react and wires instrument.ts into the app entry point. Environment-based sampling rates (production 10%, preview/dev 100%), full session replay with privacy masking, performance tracing, and data scrubbing for Matrix tokens and user IDs.

2. feat(sentry): developer settings panel and debug log attachment
Adds a SentrySettings panel under Developer Tools for manually capturing events, using the feedback widget, and controlling session replay. The debugLogger now attaches its buffered log buffer to Sentry events via breadcrumbs, extra data, and a file attachment for full diagnostic context on crash reports.

3. feat(sentry): crash page and bug report Sentry integration
DefaultErrorPage shows the Sentry event ID and offers one-click feedback submission. BugReportModal prefers the Sentry feedback dialog over GitHub issues when Sentry is configured. App.tsx wraps the router in a Sentry ErrorBoundary for automatic capture of unhandled React render errors.

4. feat(sentry): performance instrumentation across sync and messaging
Sentry spans and custom metrics across the critical paths: ClientRoot (user context via hashed MXID), ClientNonUIFeatures (push notifications), DirectDMsList (DM sync), RoomTimeline (jump-load and pagination latency with sable.timeline.jump_load_ms metric), RoomInput (message send with threading context), EncryptedContent (per-event decryption latency), matrix.ts/room.ts (Matrix API calls), slidingSync.ts (sync cycle spans).

5. docs(sentry): privacy information, integration guide, and CI environment configuration
Comprehensive docs/SENTRY_INTEGRATION.md covering setup, environment variables, sampling rates, privacy configuration, and self-hosting. CI workflows pass VITE_SENTRY_DSN and VITE_SENTRY_ENVIRONMENT to Cloudflare deploy and OpenTofu actions.

Setup

Sentry is opt-in: the integration is a no-op unless VITE_SENTRY_DSN is set at build time. Self-hosters who do not set this variable are unaffected.

Environment variable reference:

Variable Required Notes
VITE_SENTRY_DSN Yes Sentry project DSN
VITE_SENTRY_ENVIRONMENT No production (10% sampling) or preview (100%)

See docs/SENTRY_INTEGRATION.md for full details.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 15, 2026

OpenTofu plan for production

Plan: 2 to add, 0 to change, 2 to destroy.
OpenTofu used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

OpenTofu will perform the following actions:

  # cloudflare_worker_version.site must be replaced
-/+ resource "cloudflare_worker_version" "site" {
!~      annotations         = {
+           workers_message      = (known after apply)
+           workers_tag          = (known after apply)
!~          workers_triggered_by = "create_version_api" -> (known after apply)
        } -> (known after apply)
!~      assets              = { # forces replacement
!~          asset_manifest_sha256 = "b44bbf6a507cd60278d078f529f5e4b4bfe36580139ecb1ff4fc842de520526f" -> "dabe9263bcfbd3acf488419df0f30df7f446fd96b3307ddca33789bb563681ce" # forces replacement
!~          directory             = "/home/runner/work/Sable/Sable/dist" -> "/github/workspace/dist"
#            (1 unchanged attribute hidden)
        }
+       compatibility_flags = (known after apply)
!~      created_on          = "2026-03-14T23:24:54Z" -> (known after apply)
!~      id                  = "************************************" -> (known after apply)
+       limits              = (known after apply)
+       main_script_base64  = (known after apply)
!~      number              = 112 -> (known after apply)
!~      source              = "terraform" -> (known after apply)
+       startup_time_ms     = (known after apply)
#        (4 unchanged attributes hidden)
    }

  # cloudflare_workers_deployment.site must be replaced
-/+ resource "cloudflare_workers_deployment" "site" {
!~      annotations  = { # forces replacement
!~          workers_message      = "b90555a27c7dac104a96bf0e2e5b66c2c815da2f" -> "feat(sentry): error tracking, performance monitoring, and session replay"
!~          workers_triggered_by = "deployment" -> (known after apply)
        }
!~      author_email = "cloudflare@sl.sable.moe" -> (known after apply)
!~      created_on   = "2026-03-14T23:24:55Z" -> (known after apply)
!~      id           = "************************************" -> (known after apply)
!~      source       = "terraform" -> (known after apply)
!~      versions     = [ # forces replacement
!~          {
!~              version_id = "************************************" -> (known after apply)
#                (1 unchanged attribute hidden)
            },
        ]
#        (3 unchanged attributes hidden)
    }

Plan: 2 to add, 0 to change, 2 to destroy.

Warning: Attribute Deprecated

  with cloudflare_workers_custom_domain.site,
  on main.tf line 41, in resource "cloudflare_workers_custom_domain" "site":
  41:   environment = "production"

This attribute is deprecated.

(and one more similar warning elsewhere)

📝 Plan generated in Cloudflare Infra #53

@Just-Insane Just-Insane force-pushed the feat/sentry-pr branch 4 times, most recently from 47151fc to 39a4ca1 Compare March 15, 2026 06:31
Add @sentry/react and wire instrument.ts into the app entry point.

- Environment detection: production (10%), preview (100%), development
  (100%); driven by VITE_SENTRY_ENVIRONMENT env var
- SessionReplay with full privacy masking (text, media, inputs)
- Performance tracing and profiling enabled
- Data scrubbing for Matrix tokens and user IDs
- Exposes Sentry on window.Sentry for console-level debugging
- SentrySettings panel: capture events manually, feedback widget,
  session replay controls, and diagnostic test buttons
- Wire SentrySettings into DevelopTools tab
- debugLogger: attach buffered logs to Sentry events via breadcrumbs,
  extra data, and file attachment for full context on error reports
- DefaultErrorPage: show Sentry event ID, offer one-click feedback
  submission for in-field crash reports
- BugReportModal: prefer Sentry feedback dialog over GitHub issues
  when Sentry is configured; include all bug report fields
- App.tsx: wrap router in Sentry ErrorBoundary for automatic
  capture of unhandled React rendering errors
Add Sentry spans and custom metrics throughout the critical paths:

- ClientRoot: set user context (hashed MXID) and identify session
- ClientNonUIFeatures: instrument push notification handling
- DirectDMsList: DM sync performance metrics
- RoomTimeline: spans for jump-load and pagination with direction
  and encryption attributes; sable.timeline.jump_load_ms metric
- RoomInput: message send span with threading context
- EncryptedContent: decryption latency span per event
- matrix.ts / room.ts: Matrix API call instrumentation
- slidingSync.ts: sliding sync cycle performance spans
- docs/SENTRY_INTEGRATION.md: comprehensive guide covering setup,
  environment variables, sampling rates, privacy configuration,
  and self-hosting considerations
- cloudflare-web-deploy.yml: pass VITE_SENTRY_DSN and
  VITE_SENTRY_ENVIRONMENT=production to deploy workflow
- prepare-tofu/action.yml: forward Sentry env vars through
  OpenTofu infrastructure actions
- changesets for Sentry integration and improved crash page
…red log attrs

- vite.config.ts: enable reactComponentAnnotation in sentryVitePlugin so React
  component names appear in breadcrumbs, spans, and replay search instead of
  raw minified CSS selectors
- debugLogger.ts: include flattened primitive fields from entry.data as
  searchable attributes in Sentry.logger.* structured log calls; previously
  only category/namespace were passed, dropping all contextual data
- instrument.ts: add beforeSendLog to scrub Matrix IDs/tokens from structured
  log messages in production; drop debug-level logs in production; set global
  scope attributes (app.name, app.version) on all events/logs
- loginUtil.ts: wrap login() in Sentry.startSpan({op: auth}) to track login
  latency and attach auth.method/auth.error/auth.success span attributes;
  covers both password and SSO token login paths
- Router.tsx: add scoped Sentry.ErrorBoundary with beforeCapture section tags
  around auth routes (section=auth) and client routes (section=client) for
  better error grouping and filtering in Sentry
slidingSync.ts:
- Add `sync.initial` span from attach() to first SlidingSyncState.Complete,
  with attributes sync.cycles_to_ready and sync.rooms_at_ready. Replaces the
  misleading sable.sync.initial_ms metric which previously measured only the
  processing time within the lifecycle callback (effectively ~0ms) rather than
  the actual wall-clock wait from attach() to first data.
- Fix sable.sync.initial_ms to use attachTime (wall-clock from attach()) so
  the metric reflects real user-perceived latency.
- Add sable.sync.lists_loaded_ms distribution (attach() to all lists fully
  loaded) and sable.sync.total_rooms gauge emitted when listsFullyLoaded first
  becomes true.
- Add sable.sync.active_subscriptions gauge in subscribeToRoom /
  unsubscribeFromRoom to track how many rooms have active subscriptions.
- Wrap probe() static method in Sentry.startSpan (sync.probe, op=matrix.sync)
  with probe.supported span attribute.
- Wrap startSpidering() loop in Sentry.startSpan (sync.spidering,
  op=matrix.sync) with spidering.batches and spidering.total_rooms attributes.

initMatrix.ts:
- Add sable.sync.transport count metric in startClassicSync and sliding sync
  active path, with transport, reason, and fallback attributes. Allows filtering
  Sentry dashboards by sync transport in use.
Add Sentry metrics to surface orphaned resources, growing caches, and
unexpectedly long waits before they become user-visible problems.

src/app/hooks/useBlobCache.ts
- Export getBlobCacheStats() returning { cacheSize, inflightCount } from
  the module-level Maps so callsites can observe cache growth over time.

src/app/state/callEmbed.ts
- Import Sentry and track embedCreatedAt when a CallEmbed is promoted.
- On disposal or replacement emit sable.call.embed_lifetime_ms (tagged
  disposed/replaced) to detect embeds whose dispose path was skipped.

src/client/initMatrix.ts
- Instrument waitForClientReady: emit sable.sync.client_ready_ms
  distribution with timed_out attribute on every call path.
- When the timeout fires add a warning-level breadcrumb so a stuck-sync
  state is visible in the context of related error events.

src/app/pages/client/ClientNonUIFeatures.tsx
- Add HealthMonitor component rendered inside ClientNonUIFeatures.
  60s setInterval emits:
    sable.media.blob_cache_size  (gauge) - cached object-URL count
    sable.media.inflight_requests (gauge) - inflight fetches when > 0
  Adds a warning breadcrumb when inflightCount >= 10 (stuck fetches).

src/app/pages/client/BackgroundNotifications.tsx
- Add Sentry import.
- Emit sable.background.client_count gauge after each add and remove of
  a background Matrix client for multi-account visibility.
- beforeBreadcrumb: replace overly-broad single-char patterns ('@','!','$')
  with precise Matrix ID regexes that always apply to every breadcrumb message,
  ensuring @user:server, !room:server and $eventId are consistently redacted
  rather than only when the broad check happened to fire

- beforeSend: fingerprint MatrixError instances by their errcode so that
  M_FORBIDDEN, M_NOT_FOUND, M_LIMIT_EXCEEDED etc. land in distinct Sentry
  issues instead of all being merged under the same stack-trace group

- SentryRoomContextFeature: new component that tracks lastVisitedRoomIdAtom
  and writes a 'room' context (type, encrypted, member_count_range) plus
  room_type/room_encrypted tags to the scope whenever the active room changes;
  these appear on every subsequent error captured while the room is open

- setUser: add serverDomain as 'username' alongside the hashed user ID so
  issues can be segmented by homeserver without exposing personal identifiers
…lures

- SyncStatus: add Sentry breadcrumb + sable.sync.degraded metric count when
  SyncState transitions to Reconnecting (warning) or Error (error); includes
  previous state in breadcrumb data for context

- ClientRoot useLogoutListener: capture breadcrumb before clearing stores on
  HttpApiEvent.SessionLoggedOut (server-side forced session expiry/revocation)
  so the event appears in Sentry before the page reloads and session is lost

- useKeyBackup useKeyBackupSync: add error breadcrumb + sable.crypto.key_backup_failures
  metric (keyed by errcode) when CryptoEvent.KeyBackupFailed fires, giving
  visibility into E2E key backup reliability
…ilures

- EncryptedContent: include event.decryptionFailureReason as an attribute on
  sable.decryption.failure so UTD spikes can be triaged by cause
  (MISSING_ENCRYPTION_KEY, UNKNOWN_MESSAGE_INDEX,
  SENDER_IDENTITY_VERIFICATION_VIOLATION, etc.) rather than a single bucket

- initMatrix wipeAllStores: add warning breadcrumb + sable.crypto.store_wipe
  metric when a store mismatch forces a local db wipe-and-retry; fires for
  both buildClient and initRustCrypto mismatch paths

- RoomTimeline handleLocalEchoUpdated: emit sable.message.send_failed count
  metric when a local echo transitions to EventStatus.NOT_SENT, giving
  visibility into outbound message delivery failures

- loginUtil login(): emit sable.auth.login_failed count metric keyed by
  errcode alongside the existing span attribute so login pressure is
  visible in Metrics without querying trace data
- Tag all Sentry events with PR number via VITE_SENTRY_PR env var
  (instrument.ts: setTag('pr', prNumber) on global scope)
- cloudflare-web-preview.yml: inject Sentry DSN, environment=preview,
  PR number, and source-map secrets into build env for PR runs
- New workflow sentry-preview-issues.yml: on every PR push, query Sentry
  for unresolved issues tagged with the PR number and environment=preview;
  create a GitHub issue per unique error (deduplicated by sentry-id marker),
  labelled 'sentry-preview' + 'pr-{N}'; maintain a sticky PR comment
  with a summary table; reopen closed issues on regression
- Labels allow filtering: -label:sentry-preview hides all automated issues
@Just-Insane Just-Insane marked this pull request as ready for review March 15, 2026 08:55
@Just-Insane Just-Insane requested a review from a team March 15, 2026 08:55
- instrument.ts: remove useless escape \[ in character class regexes
- matrix.ts, RoomInput.tsx: fix @sentry/react import order (third-party
  must precede local imports per import-x/order)
- debugLogger.ts: replace for..of with Object.entries().forEach()
  (no-restricted-syntax)
- ClientNonUIFeatures.tsx: replace nested ternary with if/else for
  memberCountRange (no-nested-ternary)
- SentrySettings.tsx: remove useless Fragment wrapper; replace template
  literal with string literal where no interpolation is needed
- Prettier format: apply auto-formatting to all touched files
Updated quotes in SENTRY_INTEGRATION.md for consistency.
# Conflicts:
#	src/app/pages/client/sidebar/DirectDMsList.tsx
- Add docs/PRIVACY_POLICY.md with full privacy policy including data
  collection details, third-party services (Sentry), and user controls
- Add 'Diagnostics & Privacy' section to General settings with Error
  Reporting and Session Replay toggles (now accessible without enabling
  Developer Tools)
- Remove basic toggles from SentrySettings in Developer Tools; keep
  only advanced/developer config (breadcrumbs, metrics, debug log export)
- Add note in SentrySettings pointing to General for main toggles
- Update settings path references in SENTRY_PRIVACY.md and
  SENTRY_INTEGRATION.md from 'Developer Tools' to 'General'
…ion values

- Add .replace(/(\/preview_url)\?[^#\s]*/gi, '$1') to scrubMatrixUrl() so the
  full external URL in ?url= is stripped from preview_url breadcrumbs and spans
- Apply scrubMatrixUrl() to exception.value strings in beforeSend so embedded
  URLs in MatrixError messages (e.g. 'Got error 403 (/preview_url?url=etsy.com/...)')
  are also redacted before sending to Sentry
@github-actions
Copy link
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

Status Preview URL Commit Alias Updated (UTC)
✅ Deployment successful! https://pr-280-sable.raspy-dream-bb1d.workers.dev 99ff314 pr-280 Sun, 15 Mar 2026 20:29:02 GMT

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant