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 @@ + + +
+
+ + + + +
500
+

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 %> + +
✓ Copied to clipboard!
+
+ <% end %> + + Go back home +
+
diff --git a/app/views/errors/not_found.html.erb b/app/views/errors/not_found.html.erb new file mode 100644 index 0000000..042f7a9 --- /dev/null +++ b/app/views/errors/not_found.html.erb @@ -0,0 +1,100 @@ + + +
+
+ + + + +
404
+

Page Not Found

+

The page you were looking for doesn't exist.

+ + <% if @event_id.present? %> +
+

Error ID:

+ + <%= @event_id %> + +
✓ Copied to clipboard!
+
+ <% end %> + + Go back home +
+
diff --git a/app/views/errors/unprocessable_entity.html.erb b/app/views/errors/unprocessable_entity.html.erb new file mode 100644 index 0000000..b680c63 --- /dev/null +++ b/app/views/errors/unprocessable_entity.html.erb @@ -0,0 +1,100 @@ + + +
+
+ + + + +
422
+

Unprocessable Entity

+

The change you wanted was rejected.

+ + <% if @event_id.present? %> +
+

Error ID:

+ + <%= @event_id %> + +
✓ Copied to clipboard!
+
+ <% end %> + + Go back home +
+
diff --git a/app/views/layouts/errors.html.erb b/app/views/layouts/errors.html.erb new file mode 100644 index 0000000..56cb3ab --- /dev/null +++ b/app/views/layouts/errors.html.erb @@ -0,0 +1,36 @@ + + + + <%= content_for(:title) || "Error - Hack Club Auth" %> + + + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + + + + <%= vite_client_tag %> + <%= vite_stylesheet_tag "application.css" %> + <%= vite_javascript_tag 'application' %> + + + <%= yield %> + <% unless Rails.env.production? %> +
+ <% end %> + + diff --git a/app/views/shared/_flash.html.erb b/app/views/shared/_flash.html.erb index 04e4498..ff4fd59 100644 --- a/app/views/shared/_flash.html.erb +++ b/app/views/shared/_flash.html.erb @@ -1,6 +1,7 @@ <% flash.each do |type, message| %> + <% next if type == "sentry_event_id" %> <% if message.present? %> - <%= render Components::Banner.new(kind: type) do %> + <%= render Components::Banner.new(kind: type, event_id: %w[error alert danger].include?(type) ? flash[:sentry_event_id] : nil) do %> <%= message %> <% end %> <% end %> diff --git a/config/application.rb b/config/application.rb index c9f831d..ed6ad6e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -49,9 +49,12 @@ module IdentityVault unless Rails.env.development? config.rails_semantic_logger.add_file_appender = false config.semantic_logger.add_appender(io: $stdout, formatter: :json) - config.semantic_logger.add_appender(appender: :honeybadger_insights) + config.semantic_logger.add_appender(appender: :sentry_ruby) end + # Use dynamic error pages to display Sentry event IDs + config.exceptions_app = self.routes + config.to_prepare do Doorkeeper::ApplicationController.layout "logged_out" Doorkeeper::ApplicationController.skip_before_action :authenticate_identity! diff --git a/config/honeybadger.yml b/config/honeybadger.yml deleted file mode 100644 index cb5a8cc..0000000 --- a/config/honeybadger.yml +++ /dev/null @@ -1,39 +0,0 @@ ---- -# For more options, see https://docs.honeybadger.io/lib/ruby/gem-reference/configuration - -api_key: '<%= ENV["HONEYBADGER_API_KEY"] %>' - -# The environment your app is running in. -env: "<%= Rails.env %>" - -# The absolute path to your project folder. -root: "<%= Rails.root.to_s %>" - -# Honeybadger won't report errors in these environments. -development_environments: -- test -- development -- cucumber - -# By default, Honeybadger won't report errors in the development_environments. -# You can override this by explicitly setting report_data to true or false. -# report_data: true - -# The current Git revision of your project. Defaults to the last commit hash. -# revision: null - -# Enable verbose debug logging (useful for troubleshooting). -debug: false - -# Enable Honeybadger Insights -insights: - enabled: true -rails: - insights: - metrics: true -net_http: - insights: - metrics: true -puma: - insights: - metrics: true diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb new file mode 100644 index 0000000..ed3d8dc --- /dev/null +++ b/config/initializers/sentry.rb @@ -0,0 +1,18 @@ +git_hash = ENV["SOURCE_COMMIT"] || `git rev-parse HEAD` rescue "unknown" +short_hash = git_hash[0..7] +is_dirty = `git status --porcelain`.strip.length > 0 rescue false +git_version = is_dirty ? "#{short_hash}-dirty" : short_hash + +Sentry.init do |config| + config.dsn = ENV["SENTRY_DSN"] + config.environment = Rails.env + config.release = git_version + config.enabled_environments = %w[production staging uat] + config.breadcrumbs_logger = [ :active_support_logger, :http_logger ] + config.send_default_pii = true + config.traces_sample_rate = Rails.env.production? ? 0.1 : 1.0 + config.enabled_patches = [ :http, :redis, :puma ] + + # Capture error event IDs in Rack env for error pages + config.rails.report_rescued_exceptions = true +end diff --git a/config/routes.rb b/config/routes.rb index b9c623b..4da6ca6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -367,6 +367,11 @@ Rails.application.routes.draw do get "/auth", to: "saml#sp_initiated_get" end + # Error pages + match "/404", to: "errors#not_found", via: :all + match "/422", to: "errors#unprocessable_entity", via: :all + match "/500", to: "errors#internal_server_error", via: :all + # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. get "up" => "rails/health#show", as: :rails_health_check diff --git a/public/404.html b/public/404.html deleted file mode 100644 index c0670bc..0000000 --- a/public/404.html +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - - The page you were looking for doesn’t exist (404 Not found) - - - - - - - - - - - - - -
-
- -
-
-

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.

-
-
- - - - diff --git a/public/422.html b/public/422.html deleted file mode 100644 index 8bcf060..0000000 --- a/public/422.html +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - - The change you wanted was rejected (422 Unprocessable Entity) - - - - - - - - - - - - - -
-
- -
-
-

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.

-
-
- - - - diff --git a/public/500.html b/public/500.html deleted file mode 100644 index d77718c..0000000 --- a/public/500.html +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - - We’re sorry, but something went wrong (500 Internal Server Error) - - - - - - - - - - - - - -
-
- -
-
-

We’re sorry, but something went wrong.
If you’re the application owner check the logs for more information.

-
-
- - - -