diff --git a/Gemfile b/Gemfile index a1a44a4aa..b95598e8e 100755 --- a/Gemfile +++ b/Gemfile @@ -113,7 +113,9 @@ gem 'ed25519', '>= 1.2', '< 2.0' # gem 'bcrypt_pbkdf', '>= 1.0', '< 2.0' # SSH key encryption support for deployment gem 'sitemap_generator' # Sitemap generator gem 'newrelic_rpm', '>=3.3.0' # Performance monitoring -gem "sentry-raven" # Error tracking +gem "sentry-ruby" # Error tracking +gem "sentry-rails" # Error tracking - Rails integration +gem "sentry-sidekiq" # Error tracking - Sidekiq worker errors ############################################################ diff --git a/Gemfile.lock b/Gemfile.lock index a046701c0..4437a964b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -661,8 +661,16 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - sentry-raven (3.1.2) - faraday (>= 1.0) + sentry-rails (6.7.0) + railties (>= 5.2.0) + sentry-ruby (~> 6.7.0) + sentry-ruby (6.7.0) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + logger + sentry-sidekiq (6.7.0) + sentry-ruby (~> 6.7.0) + sidekiq (>= 5.0) shellany (0.0.1) sidekiq (7.3.10) base64 @@ -861,7 +869,9 @@ DEPENDENCIES sass-rails (>= 6.0) sassc-rails selenium-webdriver - sentry-raven + sentry-rails + sentry-ruby + sentry-sidekiq sidekiq (~> 7.3) sidekiq-cron (~> 2.0) sitemap_generator diff --git a/app/lib/bible_gateway.rb b/app/lib/bible_gateway.rb index 1dcd2a5fc..a5d671a7b 100644 --- a/app/lib/bible_gateway.rb +++ b/app/lib/bible_gateway.rb @@ -62,7 +62,7 @@ def lookup(passage) retry end - Raven.tags_context(service: 'bible_gateway', http_status: e.io&.status&.first) if defined?(Raven) + Sentry.set_tags(service: 'bible_gateway', http_status: e.io&.status&.first) if defined?(Sentry) Rails.logger.warn("BibleGateway lookup failed: #{e.message}") if defined?(Rails) { title: '--', content: '--' } rescue StandardError => e diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index f5b8c46a2..1ea324e3e 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -1,6 +1,10 @@ - # Error tracking - Raven.configure do |config| - config.dsn = 'https://a1106f25de724396a866c6ab9386b11b:cead6cb038b44236b6e3e43887faf76d@sentry.io/299442' - config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s) - config.environments = %w[ production ] - end \ No newline at end of file +# Error tracking. +# Release is intentionally NOT set here: sentry-ruby auto-detects it — +# SENTRY_RELEASE env var, then the local git SHA, then Capistrano's REVISION +# file — so production events are tagged with the exact deployed commit +# (Capistrano release dirs have no .git, so REVISION wins in production). +Sentry.init do |config| + config.dsn = 'https://a1106f25de724396a866c6ab9386b11b@sentry.io/299442' + config.enabled_environments = %w[production] + config.breadcrumbs_logger = [:active_support_logger] +end diff --git a/documentation/plans/2026-08-11-sentry-release-tracking.md b/documentation/plans/2026-08-11-sentry-release-tracking.md new file mode 100644 index 000000000..bc4c21ce0 --- /dev/null +++ b/documentation/plans/2026-08-11-sentry-release-tracking.md @@ -0,0 +1,254 @@ +# Sentry Release Tracking Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the EOL `sentry-raven` SDK with `sentry-ruby`/`sentry-rails`/`sentry-sidekiq` so every production error is tagged with the deployed git SHA (read from Capistrano's `REVISION` file by the SDK's built-in release detection). + +**Architecture:** Three self-contained changes: (1) swap the gems and rewrite `config/initializers/sentry.rb` as `Sentry.init` with behavior parity (production-only, no PII); (2) migrate the single legacy `Raven.tags_context` call site in `BibleGateway`; (3) verify with the full test suites. No deploy-side changes: Capistrano already writes `REVISION`, which the modern SDK detects natively. + +**Tech Stack:** Rails 7.2.3.1, Ruby 3.2.6, RSpec, sentry-ruby/sentry-rails/sentry-sidekiq (6.7.0, latest from RubyGems at implementation time). + +**Spec:** `documentation/specs/2026-08-11-sentry-release-tracking-design.md` + +## Global Constraints + +- Branch: `chore/sentry-sdk-migration` (already created; spec committed on it). +- 100% of tests must pass — RSpec, Vitest, AND Cucumber — before claiming any task complete (project policy, CLAUDE.md). +- RSpec and Cucumber share the test database: run them **serially, never concurrently**. +- Sentry must send events **only** from the `production` environment (parity with the old raven config). +- No PII in events: `send_default_pii` stays `false` (SDK default — do not set it to true). +- DSN (modern format, no secret key): `https://a1106f25de724396a866c6ab9386b11b@sentry.io/299442` +- Do NOT set `config.release` explicitly — the SDK's built-in detection (SENTRY_RELEASE env → git SHA → Capistrano `REVISION` file) is the feature under delivery. +- Do NOT stage `node_modules/` churn in any commit (only `package.json` + `package-lock.json` would ever be committed for npm changes; this plan makes no npm changes). +- Commit messages end with: `Co-Authored-By: Claude Fable 5 ` + +--- + +### Task 1: Swap SDK gems and rewrite the Sentry initializer + +**Files:** +- Modify: `Gemfile:116` (the `gem "sentry-raven"` line) +- Modify: `Gemfile.lock` (via `bundle install` — never by hand) +- Rewrite: `config/initializers/sentry.rb` +- Create: `spec/initializers/sentry_spec.rb` + +**Interfaces:** +- Consumes: nothing from other tasks. +- Produces: a booted app where the global `Sentry` module is initialized in every environment (sending gated to production). Task 2 relies on `Sentry.set_tags(hash)` being callable and on `defined?(Sentry)` being truthy. + +- [ ] **Step 1: Write the failing spec** + +Create `spec/initializers/sentry_spec.rb` (new directory `spec/initializers/` — precedent: `spec/middleware/`): + +```ruby +require 'rails_helper' + +RSpec.describe 'Sentry configuration' do + let(:config) { Sentry.configuration } + + it 'is initialized' do + expect(Sentry.initialized?).to be true + end + + it 'sends events only from production' do + expect(config.enabled_environments).to eq(['production']) + end + + it 'points at the memverse Sentry project' do + expect(config.dsn.public_key).to eq('a1106f25de724396a866c6ab9386b11b') + expect(config.dsn.project_id.to_s).to eq('299442') + expect(config.dsn.host).to eq('sentry.io') + end + + it 'does not send personally identifiable information' do + expect(config.send_default_pii).to be false + end + + it 'records ActiveSupport breadcrumbs' do + expect(config.breadcrumbs_logger).to include(:active_support_logger) + end +end +``` + +- [ ] **Step 2: Run the spec to verify it fails** + +Run: `bundle exec rspec spec/initializers/sentry_spec.rb` +Expected: FAIL — `NameError: uninitialized constant Sentry` (the raven gem defines `Raven`, not `Sentry`). + +- [ ] **Step 3: Swap the gems** + +In `Gemfile`, replace the single line (currently line 116): + +```ruby +gem "sentry-raven" # Error tracking +``` + +with: + +```ruby +gem "sentry-ruby" # Error tracking +gem "sentry-rails" # Error tracking - Rails integration +gem "sentry-sidekiq" # Error tracking - Sidekiq worker errors +``` + +Then run: `bundle install` +Expected: resolves cleanly; `sentry-raven` removed from `Gemfile.lock`, three `sentry-*` gems (same version, 6.7.0) added. `faraday` remains in the lockfile (other gems still need it) — that is fine. + +- [ ] **Step 4: Rewrite the initializer** + +Replace the entire contents of `config/initializers/sentry.rb` with: + +```ruby +# Error tracking. +# Release is intentionally NOT set here: sentry-ruby auto-detects it from +# SENTRY_RELEASE, then Capistrano's REVISION file, then the local git SHA — +# so production events are tagged with the exact deployed commit. +Sentry.init do |config| + config.dsn = 'https://a1106f25de724396a866c6ab9386b11b@sentry.io/299442' + config.enabled_environments = %w[production] + config.breadcrumbs_logger = [:active_support_logger] +end +``` + +- [ ] **Step 5: Run the spec to verify it passes** + +Run: `bundle exec rspec spec/initializers/sentry_spec.rb` +Expected: 5 examples, 0 failures. + +- [ ] **Step 6: Verify no-PII parity with the old sanitize_fields behavior** + +The old raven config sent sanitized request bodies; the modern SDK must not send bodies/cookies at all with `send_default_pii = false`. Verify against the installed gem source: + +Run: `grep -n -A6 "send_default_pii" "$(bundle show sentry-ruby)/lib/sentry/interfaces/request.rb"` +Expected: request `data` (body) and `cookies` are populated **only** inside the `send_default_pii` branch. + +Decision rule: if (and only if) the installed version populates request data unconditionally, add this inside the `Sentry.init` block and re-run Step 5: + +```ruby + filter = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters) + config.before_send = lambda do |event, _hint| + event.request.data = filter.filter(event.request.data) if event.request&.data.is_a?(Hash) + event + end +``` + +- [ ] **Step 7: Smoke-check release detection wiring** + +Run: `bundle exec rails runner "puts Sentry.configuration.release.inspect"` +Expected: prints `nil` — on sentry-ruby 6.x, release detection only runs where sending is allowed (production), so `nil` outside production is the correct result; an exception is a failure. + +- [ ] **Step 8: Commit** + +```bash +git add Gemfile Gemfile.lock config/initializers/sentry.rb spec/initializers/sentry_spec.rb +git commit -m "Replace EOL sentry-raven with sentry-ruby/rails/sidekiq for release tracking + +Co-Authored-By: Claude Fable 5 " +``` + +--- + +### Task 2: Migrate the BibleGateway Raven call site + +**Files:** +- Modify: `app/lib/bible_gateway.rb:65` +- Modify: `spec/lib/bible_gateway_spec.rb` + +**Interfaces:** +- Consumes: initialized `Sentry` module from Task 1 (`Sentry.set_tags(hash)`). +- Produces: nothing consumed by later tasks. + +- [ ] **Step 1: Add the failing test** + +In `spec/lib/bible_gateway_spec.rb`, add inside `describe '#lookup'`: + +```ruby + it 'tags Sentry with service context on persistent HTTP failures' do + io = double('io', status: ['503', 'Service Unavailable']) + error = OpenURI::HTTPError.new('503 Service Unavailable', io) + + allow(URI).to receive(:open).and_raise(error) + allow(gateway).to receive(:sleep) # skip retry backoff delays + allow(Sentry).to receive(:set_tags) + + gateway.lookup('John 3:16') + + expect(Sentry).to have_received(:set_tags) + .with(service: 'bible_gateway', http_status: '503') + end +``` + +- [ ] **Step 2: Run the spec file to verify the new test fails** + +Run: `bundle exec rspec spec/lib/bible_gateway_spec.rb` +Expected: 3 examples, 1 failure — the new test fails with "expected Sentry to have received set_tags" (the code still guards on `defined?(Raven)`, which is now false, so nothing is called). + +- [ ] **Step 3: Migrate the call site** + +In `app/lib/bible_gateway.rb`, replace line 65: + +```ruby + Raven.tags_context(service: 'bible_gateway', http_status: e.io&.status&.first) if defined?(Raven) +``` + +with: + +```ruby + Sentry.set_tags(service: 'bible_gateway', http_status: e.io&.status&.first) if defined?(Sentry) +``` + +- [ ] **Step 4: Run the spec file to verify all pass** + +Run: `bundle exec rspec spec/lib/bible_gateway_spec.rb` +Expected: 3 examples, 0 failures. + +- [ ] **Step 5: Verify no Raven references remain in app code** + +Run: `grep -rn "Raven" app/ lib/ config/ --include="*.rb" --include="*.rake"` +Expected: no output. (References in `tmp/`, `documentation/`, and specs' prose descriptions are acceptable; `spec/` should also come back clean of `Raven.` API calls.) + +- [ ] **Step 6: Commit** + +```bash +git add app/lib/bible_gateway.rb spec/lib/bible_gateway_spec.rb +git commit -m "Migrate BibleGateway error tagging from Raven to Sentry API + +Co-Authored-By: Claude Fable 5 " +``` + +--- + +### Task 3: Full-suite verification + +**Files:** none created or modified (verification only). + +**Interfaces:** +- Consumes: the completed work of Tasks 1–2. +- Produces: green suites; the branch is ready for PR/merge. + +- [ ] **Step 1: Run RSpec (full)** + +Run: `bundle exec rspec` +Expected: 0 failures (baseline: ~1227 examples + 6 new = ~1233, 25 pending). Any failure must be fixed before proceeding — never proceed with failing tests. + +- [ ] **Step 2: Run Vitest** + +Run: `npm run test:run` +Expected: 409 tests passing, 0 failures (JS is untouched by this change). + +- [ ] **Step 3: Run Cucumber (after RSpec finishes — shared test DB, never concurrent)** + +Run: `bundle exec cucumber features` +Expected: 72 scenarios, 0 failures. + +- [ ] **Step 4: Summarize results in a table** (project policy) and confirm working tree is clean apart from `.claude/settings.local.json`. + +--- + +## Post-deploy verification (manual, one-time — not part of this branch) + +After the next production deploy (per spec §Post-deploy verification): + +1. On the server: `bundle exec rails console` then `Sentry.capture_message("release tracking test")`. +2. In Sentry (veetle/memverse): confirm the event arrived tagged with the deployed 40-char SHA and that a release record now exists. +3. This also settles whether the 90-day event silence was app health or broken raven delivery. diff --git a/documentation/specs/2026-08-11-sentry-release-tracking-design.md b/documentation/specs/2026-08-11-sentry-release-tracking-design.md new file mode 100644 index 000000000..4a626480f --- /dev/null +++ b/documentation/specs/2026-08-11-sentry-release-tracking-design.md @@ -0,0 +1,129 @@ +# Sentry Release Tracking — Design Spec + +**Date**: 2026-08-11 +**Status**: Approved +**Branch**: `chore/sentry-sdk-migration` + +## Problem + +Sentry cannot tell us which deploy introduced an error. The `memverse` project in +Sentry has no release records, so errors are not correlated with deployed git +revisions. With deploys that batch months of changes (e.g. the pending Rails 7.2 + +Devise 5 + jwt 3 deploy), attribution matters. + +Two underlying causes: + +1. The app uses **`sentry-raven` 3.1.2**, the legacy Sentry SDK, end-of-life since + 2021. Its Rails 7.x compatibility is unverified. +2. No release value reaches Sentry. Additionally, Sentry shows **zero error events + in the last 90 days** — production is either genuinely error-free or event + delivery is silently broken. The migration settles this either way. + +## Goal + +Every production error reaching Sentry is tagged with the git SHA that Capistrano +deployed, so errors correlate directly to deploys. + +## Decisions (made during brainstorming) + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| SDK scope | Migrate `sentry-raven` → `sentry-ruby` + `sentry-rails` + `sentry-sidekiq` | Supported SDK with first-class release detection; retires an EOL dependency; consistent with modernization roadmap | +| Release registration | Runtime tagging only (no deploy-time API call) | Reading Capistrano's `REVISION` at boot fully achieves error↔deploy correlation with zero new secrets or deploy steps | + +## Design + +### 1. Gemfile + +- Remove `gem "sentry-raven"`. +- Add `gem "sentry-ruby"`, `gem "sentry-rails"`, `gem "sentry-sidekiq"`. + +`sentry-sidekiq` is included because Sidekiq runs core background work (reminder, +quiz, and metrics workers) and the raven setup never captured worker errors. + +### 2. Initializer rewrite (`config/initializers/sentry.rb`) + +```ruby +# Error tracking +Sentry.init do |config| + config.dsn = "https://a1106f25de724396a866c6ab9386b11b@sentry.io/299442" + config.enabled_environments = %w[production] + config.breadcrumbs_logger = [:active_support_logger] + # send_default_pii stays false (default): request bodies / cookies / IPs are + # not sent. Rails filter_parameters are applied to what is sent. +end +``` + +Behavior parity with the raven config: + +- **Production-only**: `config.enabled_environments = %w[production]` replaces + raven's `config.environments`. No events, ever, from development or test. +- **Parameter sanitization**: raven used `config.sanitize_fields` built from + `Rails.application.config.filter_parameters`. sentry-rails applies Rails + parameter filtering to request data automatically; implementation must verify + this (and add explicit filtering config if the installed SDK version does not). +- The DSN's legacy secret-key portion is dropped (modern DSN format); the DSN + stays in the initializer as today. Moving it to credentials is out of scope. + +### 3. Release detection (the actual feature) + +No explicit `config.release`. The modern SDK detects the release in this order: + +1. `SENTRY_RELEASE` environment variable +2. Current git SHA (wins in development checkouts; Capistrano release + directories contain no `.git`, so this is skipped in production) +3. Capistrano `REVISION` file at the project root + +Capistrano already writes `REVISION` (full 40-char SHA) into every release +directory — production revision `f370078e9f33…` was confirmed present on +`www.memverse.com` at `current/REVISION`. So production release tagging requires +**no deploy-side changes**. Sentry creates the release record when the first +tagged event arrives. + +Verification: `Sentry.configuration.release` from a **production** console. Outside `enabled_environments` the SDK skips release detection entirely, so development/test consoles print `nil` — expected, not a failure. + +### 4. Call-site migration + +`app/lib/bible_gateway.rb:65`: + +```ruby +# before +Raven.tags_context(service: 'bible_gateway', http_status: ...) if defined?(Raven) +# after +Sentry.set_tags(service: 'bible_gateway', http_status: ...) if defined?(Sentry) +``` + +This is the only `Raven.` call site outside the initializer (verified by grep; +no spec/feature references exist). + +## Error handling + +- Missing `REVISION` (local dev): detection falls back to git SHA, then nil. + Never raises. +- `Sentry.init` with `enabled_environments` not matching current env: SDK is + loaded but sends nothing — `Sentry.set_tags` and friends remain safe no-ops. + +## Testing + +- New spec (e.g. `spec/initializers/sentry_spec.rb`) asserting: DSN configured, + `enabled_environments == ["production"]`, `send_default_pii` false. +- Full suites must pass per project policy: RSpec, Vitest, Cucumber. + +## Post-deploy verification (manual, one-time) + +After the next production deploy: + +1. `Sentry.capture_message("release tracking test")` from the production Rails + console. +2. Confirm the event appears in Sentry tagged with the deployed SHA and that a + release record now exists. + +This also settles whether the 90-day event silence was health or breakage. + +## Out of scope + +- Deploy-time Sentry Releases API notification / deploy markers (revisit if + "resolved in next release" workflows are wanted). +- Performance tracing / profiling (`traces_sample_rate` stays unset). +- Moving the DSN into Rails credentials. +- New Relic (separate monitoring stack, untouched). diff --git a/spec/initializers/sentry_spec.rb b/spec/initializers/sentry_spec.rb new file mode 100644 index 000000000..76f4b863c --- /dev/null +++ b/spec/initializers/sentry_spec.rb @@ -0,0 +1,27 @@ +require 'rails_helper' + +RSpec.describe 'Sentry configuration' do + let(:config) { Sentry.configuration } + + it 'is initialized' do + expect(Sentry.initialized?).to be true + end + + it 'sends events only from production' do + expect(config.enabled_environments).to eq(['production']) + end + + it 'points at the memverse Sentry project' do + expect(config.dsn.public_key).to eq('a1106f25de724396a866c6ab9386b11b') + expect(config.dsn.project_id.to_s).to eq('299442') + expect(config.dsn.host).to eq('sentry.io') + end + + it 'does not send personally identifiable information' do + expect(config.send_default_pii).to be false + end + + it 'records ActiveSupport breadcrumbs' do + expect(config.breadcrumbs_logger).to include(:active_support_logger) + end +end diff --git a/spec/lib/bible_gateway_spec.rb b/spec/lib/bible_gateway_spec.rb index 8600fde84..aa588682b 100644 --- a/spec/lib/bible_gateway_spec.rb +++ b/spec/lib/bible_gateway_spec.rb @@ -27,5 +27,22 @@ expect(result[:content]).to include('For God so loved the world') end + + it 'tags Sentry with service context on persistent HTTP failures' do + io = double('io', status: ['503', 'Service Unavailable']) + error = OpenURI::HTTPError.new('503 Service Unavailable', io) + + allow(URI).to receive(:open).and_raise(error) + allow(Sentry).to receive(:set_tags) + # Stubbing sleep also skips the real backoff delays + expect(gateway).to receive(:sleep).with(1).ordered + expect(gateway).to receive(:sleep).with(2).ordered + + gateway.lookup('John 3:16') + + expect(URI).to have_received(:open).exactly(3).times + expect(Sentry).to have_received(:set_tags) + .with(service: 'bible_gateway', http_status: '503') + end end end