Internal Server Error
+We're sorry, but something went wrong on our end.
+ + <% if @event_id.present? %> ++ If you need help, please provide this error ID: +
+
+ <%= @event_id %>
+
+ From 3c70ca8c2860d3f98e8688b2424fa9c1d41df49a Mon Sep 17 00:00:00 2001 From: nora <163450896+24c02@users.noreply.github.com> Date: Mon, 29 Dec 2025 16:19:10 -0500 Subject: [PATCH] switch error handling to sentry --- Gemfile | 4 +- Gemfile.lock | 12 +- app/components/banner.rb | 23 +++- app/controllers/application_controller.rb | 28 ++++- .../backend/application_controller.rb | 46 +++++-- app/controllers/errors_controller.rb | 25 ++++ app/controllers/logins_controller.rb | 11 +- app/controllers/saml_controller.rb | 11 +- app/frontend/entrypoints/application.js | 24 +++- app/services/ralsei_engine.rb | 9 +- app/services/scim_service.rb | 5 +- .../errors/internal_server_error.html.erb | 102 ++++++++++++++++ app/views/errors/not_found.html.erb | 100 +++++++++++++++ .../errors/unprocessable_entity.html.erb | 100 +++++++++++++++ app/views/layouts/errors.html.erb | 36 ++++++ app/views/shared/_flash.html.erb | 3 +- config/application.rb | 5 +- config/honeybadger.yml | 39 ------ config/initializers/sentry.rb | 18 +++ config/routes.rb | 5 + public/404.html | 114 ------------------ public/422.html | 114 ------------------ public/500.html | 114 ------------------ 23 files changed, 532 insertions(+), 416 deletions(-) create mode 100644 app/controllers/errors_controller.rb create mode 100644 app/views/errors/internal_server_error.html.erb create mode 100644 app/views/errors/not_found.html.erb create mode 100644 app/views/errors/unprocessable_entity.html.erb create mode 100644 app/views/layouts/errors.html.erb delete mode 100644 config/honeybadger.yml create mode 100644 config/initializers/sentry.rb delete mode 100644 public/404.html delete mode 100644 public/422.html delete mode 100644 public/500.html diff --git a/Gemfile b/Gemfile index 7cf8556..26faa94 100644 --- a/Gemfile +++ b/Gemfile @@ -47,7 +47,8 @@ gem "vite_rails" gem "pundit", "~> 2.5" -gem "honeybadger", "~> 5.28" +gem "sentry-ruby", "~> 5.22" +gem "sentry-rails", "~> 5.22" gem "http", "~> 5.2" @@ -152,4 +153,3 @@ end gem "premailer-rails", "~> 1.12" gem "openssl", "~> 3.3" - diff --git a/Gemfile.lock b/Gemfile.lock index 11926b9..9d2db6b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -254,9 +254,6 @@ GEM hashids (~> 1.0) hashids (1.0.6) hashie (5.0.0) - honeybadger (5.28.0) - logger - ostruct htmlentities (4.4.2) http (5.2.0) addressable (~> 2.8) @@ -537,6 +534,12 @@ GEM securerandom (0.4.1) semantic_logger (4.16.1) concurrent-ruby (~> 1.0) + sentry-rails (5.28.1) + railties (>= 5.0) + sentry-ruby (~> 5.28.1) + sentry-ruby (5.28.1) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) serve_byte_range (1.0.0) rack (>= 1.0) slack-ruby-client (2.6.0) @@ -636,7 +639,6 @@ DEPENDENCIES geocoder (~> 1.8) good_job (~> 4.10) hashid-rails (~> 1.4) - honeybadger (~> 5.28) http (~> 5.2) image_processing (~> 1.2) jb (~> 0.8.2) @@ -667,6 +669,8 @@ DEPENDENCIES rubocop-rails-omakase ruby-vips (~> 2.2) saml2 (~> 3.2) + sentry-rails (~> 5.22) + sentry-ruby (~> 5.22) slack-ruby-client (~> 2.6) slocks (~> 0.1.0) superform (~> 0.5.1) diff --git a/app/components/banner.rb b/app/components/banner.rb index 48c7023..ff787de 100644 --- a/app/components/banner.rb +++ b/app/components/banner.rb @@ -1,19 +1,38 @@ # frozen_string_literal: true class Components::Banner < Components::Base - def initialize(kind:) + def initialize(kind:, event_id: nil) @kind = kind + @event_id = event_id end def view_template(&block) div(class: "banner flex #{banner_class}") do render_icon - yield + div(class: "flex-1") do + yield + render_event_id if @event_id.present? + end end end private + def render_event_id + div(class: "error-id-container mt-1") do + small(class: "error-id-text text-sm opacity-75") do + plain "Error ID: " + code( + class: "error-id-code cursor-pointer hover:opacity-80", + data_error_id: @event_id, + onclick: "copyErrorId(this)", + title: "Click to copy" + ) { @event_id } + span(class: "copy-feedback ml-1 hidden") { " ✓ Copied!" } + end + end + end + def banner_class case @kind.to_sym when :success then "success" diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ce1593f..8c597ad 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -47,9 +47,18 @@ class ApplicationController < ActionController::Base end def set_honeybadger_context - Honeybadger.context({ - identity_id: current_identity&.id - }) + return unless current_identity + + Sentry.set_user( + id: current_identity.public_id, # Use public_id (ident!xyz) not database ID + email: current_identity.primary_email + ) + + Sentry.set_context(:identity, { + identity_public_id: current_identity.public_id, + identity_email: current_identity.primary_email, + slack_id: current_identity.slack_id + }.compact) end # Best-effort country detection from request IP; returns ISO3166 alpha-2. @@ -86,8 +95,19 @@ class ApplicationController < ActionController::Base end rescue_from ActiveRecord::RecordNotFound do |e| + event_id = Sentry.capture_exception(e) flash[:error] = "sorry, couldn't find that object... (404)" - redirect_to root_path + flash[:sentry_event_id] = event_id if event_id + redirect_to root_path unless request.path == "/" + end + + rescue_from StandardError do |e| + event_id = Sentry.capture_exception(e) + flash[:error] = "Something went wrong. Please try again." + flash[:sentry_event_id] = event_id if event_id + + raise e if Rails.env.development? + redirect_to root_path unless request.path == "/" end private diff --git a/app/controllers/backend/application_controller.rb b/app/controllers/backend/application_controller.rb index b4c2755..c0c466f 100644 --- a/app/controllers/backend/application_controller.rb +++ b/app/controllers/backend/application_controller.rb @@ -51,22 +51,54 @@ module Backend end def set_honeybadger_context - Honeybadger.context({ - user_id: current_user&.id, + return unless current_identity + + # Set user context with public_id + Sentry.set_user( + id: current_identity.public_id, # Use identity public_id (ident!xyz) + username: current_user&.username, + email: current_identity.primary_email + ) + + # Set backend user context + Sentry.set_context(:user, { user_username: current_user&.username, - identity_id: current_identity&.id, - identity_email: current_identity&.primary_email - }) + user_identity_public_id: current_user&.identity&.public_id, + user_slack_id: current_user&.slack_id, + is_super_admin: current_user&.super_admin?, + is_program_manager: current_user&.program_manager?, + can_break_glass: current_user&.can_break_glass? + }.compact) + + # Set identity context (the identity being acted upon) + Sentry.set_context(:identity, { + identity_public_id: current_identity.public_id, + identity_email: current_identity.primary_email, + slack_id: current_identity.slack_id + }.compact) + + # Set impersonation context if applicable + if current_impersonator + Sentry.set_context(:impersonation, { + impersonator_username: current_impersonator.username, + impersonator_identity_public_id: current_impersonator.identity&.public_id, + is_impersonating: true + }.compact) + end end rescue_from Pundit::NotAuthorizedError do |e| + event_id = Sentry.capture_exception(e) flash[:error] = "you don't seem to be authorized to do that?" - redirect_to backend_root_path + flash[:sentry_event_id] = event_id if event_id + redirect_to backend_root_path unless request.path == "/backend" || request.path == "/backend/" end rescue_from ActiveRecord::RecordNotFound do |e| + event_id = Sentry.capture_exception(e) flash[:error] = "sorry, couldn't find that object... (404)" - redirect_to backend_root_path + flash[:sentry_event_id] = event_id if event_id + redirect_to backend_root_path unless request.path == "/backend" || request.path == "/backend/" end end end diff --git a/app/controllers/errors_controller.rb b/app/controllers/errors_controller.rb new file mode 100644 index 0000000..05e80bb --- /dev/null +++ b/app/controllers/errors_controller.rb @@ -0,0 +1,25 @@ +class ErrorsController < ActionController::Base + layout "errors" + + def not_found + @event_id = request.env["sentry.error_event_id"] + render status: :not_found + rescue => e + # Last resort: render plain text to avoid infinite loops + render plain: "404 - Page Not Found", status: :not_found + end + + def unprocessable_entity + @event_id = request.env["sentry.error_event_id"] + render status: :unprocessable_entity + rescue => e + render plain: "422 - Unprocessable Entity", status: :unprocessable_entity + end + + def internal_server_error + @event_id = request.env["sentry.error_event_id"] + render status: :internal_server_error + rescue => e + render plain: "500 - Internal Server Error", status: :internal_server_error + end +end diff --git a/app/controllers/logins_controller.rb b/app/controllers/logins_controller.rb index 7dcc0cf..c6220d4 100644 --- a/app/controllers/logins_controller.rb +++ b/app/controllers/logins_controller.rb @@ -294,12 +294,13 @@ class LoginsController < ApplicationController slack_result else Rails.logger.error "Slack provisioning failed for #{@identity.id}: #{slack_result[:error]}" - Honeybadger.notify( + Sentry.capture_message( "Slack provisioning failed on first login", - context: { - identity_id: @identity.id, - email: @identity.primary_email, - error: slack_result[:error] + level: :error, + extra: { + identity_public_id: @identity.public_id, + identity_email: @identity.primary_email, + slack_error: slack_result[:error] } ) if scenario.should_create_slack? || @attempt.next_action == "slack" diff --git a/app/controllers/saml_controller.rb b/app/controllers/saml_controller.rb index a2df5cd..89aa366 100644 --- a/app/controllers/saml_controller.rb +++ b/app/controllers/saml_controller.rb @@ -113,12 +113,13 @@ class SAMLController < ApplicationController Rails.logger.info "Slack provisioning successful via SCIM for #{current_identity.id}: #{slack_result[:message]}" else Rails.logger.error "Slack provisioning failed via SCIM for #{current_identity.id}: #{slack_result[:error]}" - Honeybadger.notify( + Sentry.capture_message( "Slack provisioning failed via SCIM", - context: { - identity_id: current_identity.id, - email: current_identity.primary_email, - error: slack_result[:error] + level: :error, + extra: { + identity_public_id: current_identity.public_id, + identity_email: current_identity.primary_email, + slack_error: slack_result[:error] } ) flash[:error] = "We couldn't create your Slack account. Please contact support." diff --git a/app/frontend/entrypoints/application.js b/app/frontend/entrypoints/application.js index 3d8fa58..7ac0d15 100644 --- a/app/frontend/entrypoints/application.js +++ b/app/frontend/entrypoints/application.js @@ -12,4 +12,26 @@ document.addEventListener('htmx:configRequest', (event) => { if (csrfToken) { event.detail.headers['X-CSRF-Token'] = csrfToken; } -}); \ No newline at end of file +}); + +// Copy error ID to clipboard +window.copyErrorId = function(element) { + const errorId = element.dataset.errorId; + const feedback = element.nextElementSibling || element.parentElement.querySelector('.copy-feedback'); + + navigator.clipboard.writeText(errorId).then(() => { + // Show feedback + if (feedback) { + feedback.classList.add('show'); + feedback.classList.remove('hidden'); + + // Hide after 2 seconds + setTimeout(() => { + feedback.classList.remove('show'); + feedback.classList.add('hidden'); + }, 2000); + } + }).catch(err => { + console.error('Failed to copy:', err); + }); +}; \ No newline at end of file diff --git a/app/services/ralsei_engine.rb b/app/services/ralsei_engine.rb index 482d7ec..c604a2d 100644 --- a/app/services/ralsei_engine.rb +++ b/app/services/ralsei_engine.rb @@ -101,7 +101,10 @@ module RalseiEngine Rails.logger.info "RalseiEngine sent message to #{identity.slack_id} via #{channel_id} (template: #{template_name})" rescue => e Rails.logger.error "RalseiEngine failed to send message: #{e.message}" - Honeybadger.notify(e, context: { identity_id: identity.id, template: template_name }) + Sentry.capture_exception(e, extra: { + identity_public_id: identity.public_id, + ralsei_template: template_name + }) raise end @@ -128,7 +131,9 @@ module RalseiEngine dm_channel_id rescue => e Rails.logger.error "RalseiEngine failed to open DM channel: #{e.message}" - Honeybadger.notify(e, context: { identity_id: identity.id }) + Sentry.capture_exception(e, extra: { + identity_public_id: identity.public_id + }) nil end diff --git a/app/services/scim_service.rb b/app/services/scim_service.rb index a8645c2..8276417 100644 --- a/app/services/scim_service.rb +++ b/app/services/scim_service.rb @@ -154,7 +154,10 @@ module SCIMService } rescue => e Rails.logger.error "Error creating Slack user: #{e.message}" - Honeybadger.notify(e, context: { identity_id: identity.id, email: identity.primary_email }) + Sentry.capture_exception(e, extra: { + identity_public_id: identity.public_id, + identity_email: identity.primary_email + }) { success: false, diff --git a/app/views/errors/internal_server_error.html.erb b/app/views/errors/internal_server_error.html.erb new file mode 100644 index 0000000..0a5545c --- /dev/null +++ b/app/views/errors/internal_server_error.html.erb @@ -0,0 +1,102 @@ + + +
We're sorry, but something went wrong on our end.
+ + <% if @event_id.present? %> ++ If you need help, please provide this error ID: +
+
+ <%= @event_id %>
+
+ The page you were looking for doesn't exist.
+ + <% if @event_id.present? %> +Error ID:
+
+ <%= @event_id %>
+
+ The change you wanted was rejected.
+ + <% if @event_id.present? %> +Error ID:
+
+ <%= @event_id %>
+
+ The page you were looking for doesn’t exist. You may have mistyped the address or the page may have moved. If you’re the application owner check the logs for more information.
-The change you wanted was rejected. Maybe you tried to change something you didn’t have access to. If you’re the application owner check the logs for more information.
-We’re sorry, but something went wrong.
If you’re the application owner check the logs for more information.