diff --git a/app/controllers/api/hackatime/v1/hackatime_controller.rb b/app/controllers/api/hackatime/v1/hackatime_controller.rb index 76bda61..e138f55 100644 --- a/app/controllers/api/hackatime/v1/hackatime_controller.rb +++ b/app/controllers/api/hackatime/v1/hackatime_controller.rb @@ -268,8 +268,7 @@ class Api::Hackatime::V1::HackatimeController < ApplicationController queue_project_mapping(heartbeat[:project]) results << [ new_heartbeat.attributes, 201 ] rescue => e - Sentry.capture_exception(e) - Rails.logger.error("Error creating heartbeat: #{e.class.name} #{e.message}") + report_error(e, message: "Error creating heartbeat") results << [ { error: e.message, type: e.class.name }, 422 ] end @@ -284,7 +283,7 @@ class Api::Hackatime::V1::HackatimeController < ApplicationController end rescue => e # never raise an error here because it will break the heartbeat flow - Rails.logger.error("Error queuing project mapping: #{e.class.name} #{e.message}") + report_error(e, message: "Error queuing project mapping") end def check_lockout diff --git a/app/controllers/api/v1/external_slack_controller.rb b/app/controllers/api/v1/external_slack_controller.rb index 21cb661..c8d6ec5 100644 --- a/app/controllers/api/v1/external_slack_controller.rb +++ b/app/controllers/api/v1/external_slack_controller.rb @@ -53,8 +53,7 @@ module Api render json: { error: user.errors.full_messages }, status: :unprocessable_entity end rescue => e - Sentry.capture_exception(e) - Rails.logger.error "Error creating user from external Slack data: #{e.message}" + report_error(e, message: "Error creating user from external Slack data") render json: { error: "Internal server error" }, status: :internal_server_error end diff --git a/app/controllers/api/v1/stats_controller.rb b/app/controllers/api/v1/stats_controller.rb index 3673b76..5d870b3 100644 --- a/app/controllers/api/v1/stats_controller.rb +++ b/app/controllers/api/v1/stats_controller.rb @@ -278,8 +278,7 @@ class Api::V1::StatsController < ApplicationController JSON.parse(response.body)["user"]["id"] rescue => e - Sentry.capture_exception(e) - Rails.logger.error("Error finding user by email: #{e}") + report_error(e, message: "Error finding user by email") nil end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a944cef..a6a0d0b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,6 @@ class ApplicationController < ActionController::Base + include ErrorReporting + before_action :set_paper_trail_whodunnit before_action :sentry_context, if: :current_user before_action :initialize_cache_counters diff --git a/app/controllers/concerns/error_reporting.rb b/app/controllers/concerns/error_reporting.rb new file mode 100644 index 0000000..fe44000 --- /dev/null +++ b/app/controllers/concerns/error_reporting.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module ErrorReporting + extend ActiveSupport::Concern + + # Prefer this over calling Sentry and logger separately to keep reporting consistent. + # Usage: report_error(exception, message: "optional context") + def report_error(exception, message: nil, extra: {}) + Rails.logger.error(message || exception.message) + Sentry.capture_exception(exception, extra: extra.merge(message: message).compact) + end + + # Prefer this for non-exception events that still warrant Sentry visibility. + # Usage: report_message("Something bad happened", level: :error) + def report_message(message, level: :error, extra: {}) + Rails.logger.send(level, message) + Sentry.capture_message(message, level: level, extra: extra) + end +end diff --git a/app/controllers/deletion_requests_controller.rb b/app/controllers/deletion_requests_controller.rb index 7cd3386..f9ca161 100644 --- a/app/controllers/deletion_requests_controller.rb +++ b/app/controllers/deletion_requests_controller.rb @@ -11,7 +11,7 @@ class DeletionRequestsController < ApplicationController @deletion_request = DeletionRequest.create_for_user!(current_user) redirect_to deletion_path rescue ActiveRecord::RecordInvalid => e - Sentry.capture_exception(e) + report_error(e, message: "Deletion request creation failed") redirect_to my_settings_path end diff --git a/app/controllers/docs_controller.rb b/app/controllers/docs_controller.rb index 3c0ff45..36399d8 100644 --- a/app/controllers/docs_controller.rb +++ b/app/controllers/docs_controller.rb @@ -97,7 +97,7 @@ class DocsController < InertiaController format.md { render plain: content, content_type: "text/markdown" } end rescue => e - Rails.logger.error "Error loading docs: #{e.message}" + report_error(e, message: "Error loading docs") render_not_found end diff --git a/app/controllers/my/heartbeat_imports_controller.rb b/app/controllers/my/heartbeat_imports_controller.rb index edc6d8b..6e12b03 100644 --- a/app/controllers/my/heartbeat_imports_controller.rb +++ b/app/controllers/my/heartbeat_imports_controller.rb @@ -29,8 +29,7 @@ class My::HeartbeatImportsController < ApplicationController rescue HeartbeatImportRunner::InvalidProviderError, ActionController::ParameterMissing => e redirect_with_import_error(e.message) rescue => e - Sentry.capture_exception(e) - Rails.logger.error("Error starting heartbeat import for user #{current_user&.id}: #{e.message}") + report_error(e, message: "Error starting heartbeat import for user #{current_user&.id}") redirect_with_import_error("error reading file: #{e.message}") end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 7d61160..0f9bedd 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -16,8 +16,7 @@ class SessionsController < ApplicationController return end - Rails.logger.error "HCA OAuth error: #{params[:error]}" - Sentry.capture_message("HCA OAuth error: #{params[:error]}") + report_message("HCA OAuth error: #{params[:error]}") redirect_to root_path, alert: "Failed to authenticate with Hack Club Auth. Error ID: #{Sentry.last_event_id}" return end @@ -70,8 +69,7 @@ class SessionsController < ApplicationController return end - Rails.logger.error "Slack OAuth error: #{params[:error]}" - Sentry.capture_message("Slack OAuth error: #{params[:error]}") + report_message("Slack OAuth error: #{params[:error]}") redirect_to root_path, alert: "Failed to authenticate with Slack. Error ID: #{Sentry.last_event_id}" return end @@ -101,7 +99,7 @@ class SessionsController < ApplicationController redirect_to root_path, notice: "Successfully signed in with Slack! Welcome!" end else - Rails.logger.error "Failed to create/update user from Slack data" + report_message("Failed to create/update user from Slack data") redirect_to root_path, alert: "Failed to sign in with Slack" end end @@ -133,8 +131,7 @@ class SessionsController < ApplicationController redirect_uri = url_for(action: :github_create, only_path: false) if params[:error].present? - Rails.logger.error "GitHub OAuth error: #{params[:error]}" - Sentry.capture_message("GitHub OAuth error: #{params[:error]}") + report_message("GitHub OAuth error: #{params[:error]}") redirect_to my_settings_path, alert: "Failed to authenticate with GitHub. Error ID: #{Sentry.last_event_id}" return end @@ -150,7 +147,7 @@ class SessionsController < ApplicationController PosthogService.capture(@user, "github_linked") redirect_to my_settings_path, notice: "Successfully linked GitHub account!" else - Rails.logger.error "Failed to link GitHub account" + report_message("Failed to link GitHub account") redirect_to my_settings_path, alert: "Failed to link GitHub account" end end @@ -332,13 +329,13 @@ class SessionsController < ApplicationController expected_nonce = session.delete(session_key) if expected_nonce.blank? || received_nonce.blank? - Rails.logger.error("#{provider} OAuth state missing expected=#{expected_nonce.present?} received=#{received_nonce.present?}") + report_message("#{provider} OAuth state missing expected=#{expected_nonce.present?} received=#{received_nonce.present?}") return false end return true if ActiveSupport::SecurityUtils.secure_compare(received_nonce.to_s, expected_nonce.to_s) - Rails.logger.error("#{provider} OAuth state mismatch") + report_message("#{provider} OAuth state mismatch") false end end diff --git a/app/controllers/settings/access_controller.rb b/app/controllers/settings/access_controller.rb index 2640e24..c1d4945 100644 --- a/app/controllers/settings/access_controller.rb +++ b/app/controllers/settings/access_controller.rb @@ -23,8 +23,7 @@ class Settings::AccessController < Settings::BaseController render json: { token: new_api_key.token }, status: :ok end rescue => e - Sentry.capture_exception(e) - Rails.logger.error("error rotate #{e.class.name} #{e.message}") + report_error(e, message: "error rotate #{e.class.name}") render json: { error: "cant rotate" }, status: :unprocessable_entity end diff --git a/app/controllers/settings/notifications_controller.rb b/app/controllers/settings/notifications_controller.rb index 0c0d8e8..8b23797 100644 --- a/app/controllers/settings/notifications_controller.rb +++ b/app/controllers/settings/notifications_controller.rb @@ -17,8 +17,7 @@ class Settings::NotificationsController < Settings::BaseController PosthogService.capture(@user, "settings_updated", { fields: [ "weekly_summary_email_enabled" ] }) redirect_to my_settings_notifications_path, notice: "Settings updated successfully" rescue => e - Sentry.capture_exception(e) - Rails.logger.error("Failed to update notification settings: #{e.message}") + report_error(e, message: "Failed to update notification settings") flash.now[:error] = "Failed to update settings, sorry :(" render_notifications(status: :unprocessable_entity) end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index 089a619..46f9948 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -1,4 +1,6 @@ class ApplicationJob < ActiveJob::Base + include ErrorReporting + # Automatically retry jobs that encountered a deadlock # retry_on ActiveRecord::Deadlocked diff --git a/app/jobs/attempt_project_repo_mapping_job.rb b/app/jobs/attempt_project_repo_mapping_job.rb index 6e3a920..fa6e061 100644 --- a/app/jobs/attempt_project_repo_mapping_job.rb +++ b/app/jobs/attempt_project_repo_mapping_job.rb @@ -62,7 +62,7 @@ class AttemptProjectRepoMappingJob < ApplicationJob puts "repo: #{repo}" repo["html_url"] rescue JSON::ParserError => e - Rails.logger.error "Failed to parse GitHub repo response: #{e.message}" + report_error(e, message: "Failed to parse GitHub repo response") nil end @@ -79,7 +79,7 @@ class AttemptProjectRepoMappingJob < ApplicationJob parsed_response rescue JSON::ParserError => e - Rails.logger.error "Failed to parse GitHub orgs response: #{e.message}" + report_error(e, message: "Failed to parse GitHub orgs response") [] end end diff --git a/app/jobs/geocode_users_without_country_job.rb b/app/jobs/geocode_users_without_country_job.rb index 0c9fd9d..6436c23 100644 --- a/app/jobs/geocode_users_without_country_job.rb +++ b/app/jobs/geocode_users_without_country_job.rb @@ -86,8 +86,7 @@ class GeocodeUsersWithoutCountryJob < ApplicationJob return nil unless result&.country_code.present? result.country_code.upcase rescue => e - Rails.logger.error "geocode fail on #{ip}: #{e.message}" - Sentry.capture_exception(e) + report_error(e, message: "geocode fail on #{ip}") nil end @@ -98,8 +97,7 @@ class GeocodeUsersWithoutCountryJob < ApplicationJob return nil unless tz&.tzinfo&.respond_to?(:country_code) tz.tzinfo.country_code&.upcase rescue => e - Rails.logger.error "timezone geocode fail for #{timezone}: #{e.message}" - Sentry.capture_exception(e) + report_error(e, message: "timezone geocode fail for #{timezone}") nil end end diff --git a/app/jobs/heartbeat_export_job.rb b/app/jobs/heartbeat_export_job.rb index 796b967..99bb57d 100644 --- a/app/jobs/heartbeat_export_job.rb +++ b/app/jobs/heartbeat_export_job.rb @@ -72,7 +72,7 @@ class HeartbeatExportJob < ApplicationJob end end rescue ArgumentError => e - Rails.logger.error("Heartbeat export failed for user #{user_id}: #{e.message}") + report_error(e, message: "Heartbeat export failed for user #{user_id}") end private diff --git a/app/jobs/one_time/set_user_timezone_from_slack_job.rb b/app/jobs/one_time/set_user_timezone_from_slack_job.rb index a372353..5247da2 100644 --- a/app/jobs/one_time/set_user_timezone_from_slack_job.rb +++ b/app/jobs/one_time/set_user_timezone_from_slack_job.rb @@ -7,7 +7,7 @@ class OneTime::SetUserTimezoneFromSlackJob < ApplicationJob user.set_timezone_from_slack user.save! rescue => e - Rails.logger.error "Failed to update timezone for user #{user.id}: #{e.message}" + report_error(e, message: "Failed to update timezone for user #{user.id}") end end end diff --git a/app/jobs/process_account_deletions_job.rb b/app/jobs/process_account_deletions_job.rb index 1f6f4a6..d7e2481 100644 --- a/app/jobs/process_account_deletions_job.rb +++ b/app/jobs/process_account_deletions_job.rb @@ -11,8 +11,7 @@ class ProcessAccountDeletionsJob < ApplicationJob Rails.logger.info "kerblamed account ##{deletion_request.user_id}" rescue StandardError => e - Sentry.capture_exception(e, extra: { user_id: deletion_request.user_id }) - Rails.logger.error "failed to kerblam ##{deletion_request.user_id}: #{e.message}" + report_error(e, message: "failed to kerblam ##{deletion_request.user_id}", extra: { user_id: deletion_request.user_id }) Rails.logger.error e.backtrace.join("\n") end end diff --git a/app/jobs/process_commit_job.rb b/app/jobs/process_commit_job.rb index ccf8cb5..344b6a8 100644 --- a/app/jobs/process_commit_job.rb +++ b/app/jobs/process_commit_job.rb @@ -37,7 +37,7 @@ class ProcessCommitJob < ApplicationJob # when :gitlab # process_gitlab_commit(user, commit_sha, commit_api_url, repository) else - Rails.logger.error "[ProcessCommitJob] Unknown provider '#{provider_sym}' for commit #{commit_sha}." + report_message("[ProcessCommitJob] Unknown provider '#{provider_sym}' for commit #{commit_sha}.") end end @@ -61,13 +61,13 @@ class ProcessCommitJob < ApplicationJob api_commit_sha = commit_data_json["sha"] unless api_commit_sha == commit_sha - Rails.logger.error "[ProcessCommitJob] SHA mismatch for User ##{user.id}. Expected #{commit_sha}, API returned #{api_commit_sha}. URL: #{commit_api_url}" + report_message("[ProcessCommitJob] SHA mismatch for User ##{user.id}. Expected #{commit_sha}, API returned #{api_commit_sha}. URL: #{commit_api_url}") return # Critical data integrity issue end committer_date_str = commit_data_json.dig("commit", "committer", "date") unless committer_date_str - Rails.logger.error "[ProcessCommitJob] Committer date not found in API response for commit #{commit_sha}. Data: #{commit_data_json.inspect}" + report_message("[ProcessCommitJob] Committer date not found in API response for commit #{commit_sha}.") return end @@ -75,8 +75,8 @@ class ProcessCommitJob < ApplicationJob # API dates are typically ISO8601 (UTC). Time.zone.parse respects the application's zone. # It's good practice to store in UTC, which parse will do correctly for ISO8601. commit_actual_created_at = Time.zone.parse(committer_date_str) - rescue ArgumentError - Rails.logger.error "[ProcessCommitJob] Invalid committer date format '#{committer_date_str}' for commit #{commit_sha}." + rescue ArgumentError => e + report_error(e, message: "[ProcessCommitJob] Invalid committer date format '#{committer_date_str}' for commit #{commit_sha}.") return end @@ -90,7 +90,7 @@ class ProcessCommitJob < ApplicationJob Rails.logger.info "[ProcessCommitJob] Successfully processed commit #{api_commit_sha} for User ##{user.id}." elsif response.status.code == 401 # Unauthorized - Rails.logger.error "[ProcessCommitJob] Unauthorized (401) for User ##{user.id}. GitHub token expired/invalid. URL: #{commit_api_url}" + report_message("[ProcessCommitJob] Unauthorized (401) for User ##{user.id}. GitHub token expired/invalid. URL: #{commit_api_url}") user.update!(github_access_token: nil) Rails.logger.info "[ProcessCommitJob] Cleared invalid GitHub token for User ##{user.id}. User will need to re-authenticate." elsif response.status.code == 404 @@ -102,21 +102,21 @@ class ProcessCommitJob < ApplicationJob Rails.logger.warn "[ProcessCommitJob] GitHub API rate limit exceeded for User ##{user.id}. Retrying in #{delay_seconds}s. URL: #{commit_api_url}" self.class.set(wait: delay_seconds.seconds).perform_later(user.id, commit_sha, commit_api_url, "github", repository&.id) else - Rails.logger.error "[ProcessCommitJob] GitHub API forbidden (403) for User ##{user.id}. URL: #{commit_api_url}. Response: #{response.body.to_s.truncate(500)}" + report_message("[ProcessCommitJob] GitHub API forbidden (403) for User ##{user.id}. URL: #{commit_api_url}. Response: #{response.body.to_s.truncate(500)}") end else - Rails.logger.error "[ProcessCommitJob] GitHub API error for User ##{user.id}. Status: #{response.status}. URL: #{commit_api_url}. Response: #{response.body.to_s.truncate(500)}" + report_message("[ProcessCommitJob] GitHub API error for User ##{user.id}. Status: #{response.status}. URL: #{commit_api_url}. Response: #{response.body.to_s.truncate(500)}") raise "GitHub API Error: Status #{response.status}" if response.status.server_error? # Trigger retry for server errors end rescue HTTP::Error => e # Covers TimeoutError, ConnectionError - Rails.logger.error "[ProcessCommitJob] HTTP Error fetching commit #{commit_sha} for User ##{user.id}: #{e.message}. URL: #{commit_api_url}" + report_error(e, message: "[ProcessCommitJob] HTTP Error fetching commit #{commit_sha} for User ##{user.id}. URL: #{commit_api_url}") raise # Re-raise to allow GoodJob to retry based on retry_on rescue JSON::ParserError => e - Rails.logger.error "[ProcessCommitJob] JSON Parse Error for commit #{commit_sha} (User ##{user.id}): #{e.message}. URL: #{commit_api_url}. Body: #{response&.body&.to_s&.truncate(200)}" + report_error(e, message: "[ProcessCommitJob] JSON Parse Error for commit #{commit_sha} (User ##{user.id}). URL: #{commit_api_url}. Body: #{response&.body&.to_s&.truncate(200)}") # Malformed JSON usually isn't temporary, so might not retry unless API is known to be flaky. rescue ActiveRecord::RecordInvalid => e - Rails.logger.error "[ProcessCommitJob] Validation failed for commit #{commit_sha} (User ##{user.id}): #{e.message}" + report_error(e, message: "[ProcessCommitJob] Validation failed for commit #{commit_sha} (User ##{user.id})") end end diff --git a/app/jobs/pull_repo_commits_job.rb b/app/jobs/pull_repo_commits_job.rb index b9cd1a7..eb48e4d 100644 --- a/app/jobs/pull_repo_commits_job.rb +++ b/app/jobs/pull_repo_commits_job.rb @@ -44,7 +44,7 @@ class PullRepoCommitsJob < ApplicationJob process_commits(user, commits_data, repository) elsif response.status.code == 401 # Unauthorized - Rails.logger.error "[PullRepoCommitsJob] Unauthorized (401) for User ##{user.id}. GitHub token expired/invalid. URL: #{api_url}" + report_message("[PullRepoCommitsJob] Unauthorized (401) for User ##{user.id}. GitHub token expired/invalid. URL: #{api_url}") user.update!(github_access_token: nil) Rails.logger.info "[PullRepoCommitsJob] Cleared invalid GitHub token for User ##{user.id}. User will need to re-authenticate." elsif response.status.code == 404 @@ -56,18 +56,18 @@ class PullRepoCommitsJob < ApplicationJob Rails.logger.warn "[PullRepoCommitsJob] GitHub API rate limit exceeded for User ##{user.id}. Retrying in #{delay_seconds}s." self.class.set(wait: delay_seconds.seconds).perform_later(user.id, owner, repo) else - Rails.logger.error "[PullRepoCommitsJob] GitHub API forbidden (403) for User ##{user.id}. Response: #{response.body.to_s.truncate(500)}" + report_message("[PullRepoCommitsJob] GitHub API forbidden (403) for User ##{user.id}. Response: #{response.body.to_s.truncate(500)}") end else - Rails.logger.error "[PullRepoCommitsJob] GitHub API error for User ##{user.id}. Status: #{response.status}. Response: #{response.body.to_s.truncate(500)}" + report_message("[PullRepoCommitsJob] GitHub API error for User ##{user.id}. Status: #{response.status}. Response: #{response.body.to_s.truncate(500)}") raise "GitHub API Error: Status #{response.status}" if response.status.server_error? end rescue HTTP::Error => e - Rails.logger.error "[PullRepoCommitsJob] HTTP Error fetching commits for #{owner}/#{repo} (User ##{user.id}): #{e.message}" + report_error(e, message: "[PullRepoCommitsJob] HTTP Error fetching commits for #{owner}/#{repo} (User ##{user.id})") raise # Re-raise to allow GoodJob to retry based on retry_on rescue JSON::ParserError => e - Rails.logger.error "[PullRepoCommitsJob] JSON Parse Error for #{owner}/#{repo} (User ##{user.id}): #{e.message}" + report_error(e, message: "[PullRepoCommitsJob] JSON Parse Error for #{owner}/#{repo} (User ##{user.id})") raise # Re-raise to allow GoodJob to retry based on retry_on end end @@ -126,10 +126,10 @@ class PullRepoCommitsJob < ApplicationJob Rails.logger.warn "[PullRepoCommitsJob] Failed to fetch commit details for #{commit_sha}: #{commit_response.status}" end rescue HTTP::Error => e - Rails.logger.error "[PullRepoCommitsJob] HTTP Error fetching commit details for #{commit_sha}: #{e.message}" + report_error(e, message: "[PullRepoCommitsJob] HTTP Error fetching commit details for #{commit_sha}") next rescue JSON::ParserError => e - Rails.logger.error "[PullRepoCommitsJob] JSON Parse Error for commit details #{commit_sha}: #{e.message}" + report_error(e, message: "[PullRepoCommitsJob] JSON Parse Error for commit details #{commit_sha}") next end end diff --git a/app/jobs/repo_host/sync_user_events_job.rb b/app/jobs/repo_host/sync_user_events_job.rb index 10085c3..1bb8aa1 100644 --- a/app/jobs/repo_host/sync_user_events_job.rb +++ b/app/jobs/repo_host/sync_user_events_job.rb @@ -2,6 +2,8 @@ require "http" # Make sure 'http' gem is available module RepoHost class SyncUserEventsJob < ApplicationJob + include ErrorReporting + queue_as :literally_whenever # MAX_API_PAGES_TO_FETCH: Max pages to fetch. GitHub's /users/{username}/events endpoint @@ -38,7 +40,7 @@ module RepoHost # when :gitlab # process_gitlab_events else - Rails.logger.error "RepoHost::SyncUserEventsJob: Unknown provider '#{@provider_sym}' for User ##{@user.id}. Skipping." + report_message("RepoHost::SyncUserEventsJob: Unknown provider '#{@provider_sym}' for User ##{@user.id}. Skipping.") return end Rails.logger.info "Finished event sync for User ##{@user.id}, Provider: #{@provider_sym}." @@ -69,7 +71,7 @@ module RepoHost begin response = http_client_for_github.get(api_url) rescue HTTP::Error => e - Rails.logger.error "RepoHost::SyncUserEventsJob: HTTP Error for User ##{@user.id} on page #{current_page}: #{e.message}" + report_error(e, message: "RepoHost::SyncUserEventsJob: HTTP Error for User ##{@user.id} on page #{current_page}") break end @@ -141,7 +143,7 @@ module RepoHost def handle_github_api_error(response, page_number) error_details = response.parse rescue response.body.to_s.truncate(255) log_message = "RepoHost::SyncUserEventsJob: GitHub API Error for User ##{@user.id} on page #{page_number}: Status #{response.status}, Body: #{error_details}" - Rails.logger.error log_message + report_message(log_message) case response.status.code when 401 # Unauthorized @@ -158,7 +160,7 @@ module RepoHost when 422 # Unprocessable Entity - often if the user has been suspended Rails.logger.warn "GitHub API returned 422 for User ##{@user.id}. User might be suspended. Sync aborted. Details: #{error_details}" else - Rails.logger.error "Unhandled GitHub API error for User ##{@user.id}: #{response.status}. Sync aborted." + report_message("Unhandled GitHub API error for User ##{@user.id}: #{response.status}. Sync aborted.") end end end diff --git a/app/jobs/sailors_log_notify_job.rb b/app/jobs/sailors_log_notify_job.rb index ce39605..6ddb697 100644 --- a/app/jobs/sailors_log_notify_job.rb +++ b/app/jobs/sailors_log_notify_job.rb @@ -48,7 +48,7 @@ class SailorsLogNotifyJob < ApplicationJob slsn.update(sent: true) SailorsLogTeletypeJob.perform_later(message) else - Rails.logger.error("Failed to send Slack notification: #{response_data["error"]}") + report_message("Failed to send Slack notification: #{response_data["error"]}") ignorable_errors = %w[channel_not_found is_archived] if ignorable_errors.include?(response_data["error"]) # disable any preferences for this channel diff --git a/app/jobs/scan_repo_events_for_commits_job.rb b/app/jobs/scan_repo_events_for_commits_job.rb index d29f3a0..675554b 100644 --- a/app/jobs/scan_repo_events_for_commits_job.rb +++ b/app/jobs/scan_repo_events_for_commits_job.rb @@ -79,9 +79,9 @@ class ScanRepoEventsForCommitsJob < ApplicationJob potential_commits_buffer.clear end rescue JSON::ParserError => e - Rails.logger.error "[ScanRepoEventsForCommitsJob] Failed to parse raw_event_payload for Event ID #{event.id}: #{e.message}" + report_error(e, message: "[ScanRepoEventsForCommitsJob] Failed to parse raw_event_payload for Event ID #{event.id}") rescue => e # Catch other potential errors during event processing - Rails.logger.error "[ScanRepoEventsForCommitsJob] Error processing Event ID #{event.id}: #{e.message}\n#{e.backtrace.take(5).join("\n")}" + report_error(e, message: "[ScanRepoEventsForCommitsJob] Error processing Event ID #{event.id}") end # Process any remaining commits in the buffer diff --git a/app/jobs/set_user_country_code_job.rb b/app/jobs/set_user_country_code_job.rb index cced104..7aaaf09 100644 --- a/app/jobs/set_user_country_code_job.rb +++ b/app/jobs/set_user_country_code_job.rb @@ -29,8 +29,7 @@ class SetUserCountryCodeJob < ApplicationJob user.update!(country_code: country_code) end rescue => e - Rails.logger.error "timezone geocode fail for #{@user.timezone}: #{e.message}" - Sentry.capture_exception(e) + report_error(e, message: "timezone geocode fail for #{@user.timezone}") end end @@ -53,8 +52,7 @@ class SetUserCountryCodeJob < ApplicationJob result.country_code.upcase rescue => e - Rails.logger.error "geocode fail on #{ip}: #{e.message}" - Sentry.capture_exception(e) + report_error(e, message: "geocode fail on #{ip}") end end end diff --git a/app/jobs/slack_username_update_job.rb b/app/jobs/slack_username_update_job.rb index cc18df9..5be4251 100644 --- a/app/jobs/slack_username_update_job.rb +++ b/app/jobs/slack_username_update_job.rb @@ -19,7 +19,7 @@ class SlackUsernameUpdateJob < ApplicationJob user.update_from_slack user.save! rescue => e - Rails.logger.error "Failed to update Slack username and avatar for user #{user.id}: #{e.message}" + report_error(e, message: "Failed to update Slack username and avatar for user #{user.id}") end end end diff --git a/app/jobs/sync_repo_metadata_job.rb b/app/jobs/sync_repo_metadata_job.rb index 3a5d0e1..fc98e22 100644 --- a/app/jobs/sync_repo_metadata_job.rb +++ b/app/jobs/sync_repo_metadata_job.rb @@ -36,7 +36,7 @@ class SyncRepoMetadataJob < ApplicationJob raise end rescue => e - Rails.logger.error "[SyncRepoMetadataJob] Unexpected error: #{e.message}" + report_error(e, message: "[SyncRepoMetadataJob] Unexpected error") raise # Retry for other errors end end diff --git a/app/jobs/sync_stale_repo_metadata_job.rb b/app/jobs/sync_stale_repo_metadata_job.rb index 59097c8..27e973e 100644 --- a/app/jobs/sync_stale_repo_metadata_job.rb +++ b/app/jobs/sync_stale_repo_metadata_job.rb @@ -32,7 +32,7 @@ class SyncStaleRepoMetadataJob < ApplicationJob mapping.update!(repository: repo) repos_to_sync[repo.id] = repo rescue => e - Rails.logger.error "[SyncStaleRepoMetadataJob] Failed to create repository for mapping #{mapping.id}: #{e.message}" + report_error(e, message: "[SyncStaleRepoMetadataJob] Failed to create repository for mapping #{mapping.id}") end end end diff --git a/app/jobs/user_slack_status_update_job.rb b/app/jobs/user_slack_status_update_job.rb index 74e94c8..1214910 100644 --- a/app/jobs/user_slack_status_update_job.rb +++ b/app/jobs/user_slack_status_update_job.rb @@ -8,7 +8,7 @@ class UserSlackStatusUpdateJob < ApplicationJob begin user.update_slack_status rescue => e - Rails.logger.error "Failed to update Slack status for user #{user.slack_uid}: #{e.message}" + report_error(e, message: "Failed to update Slack status for user #{user.slack_uid}") end end end diff --git a/app/models/concerns/oauth_authentication.rb b/app/models/concerns/oauth_authentication.rb index 315a639..6cc2353 100644 --- a/app/models/concerns/oauth_authentication.rb +++ b/app/models/concerns/oauth_authentication.rb @@ -1,7 +1,10 @@ module OauthAuthentication extend ActiveSupport::Concern + include ErrorReporting class_methods do + include ErrorReporting + def hca_authorize_url(redirect_uri) params = { redirect_uri:, @@ -132,8 +135,7 @@ module OauthAuthentication user.save! user rescue => e - Rails.logger.error "Error creating user from Slack data: #{e.message}" - Rails.logger.error e.backtrace.join("\n") + report_error(e, message: "Error creating user from Slack data: #{e.message}") nil end @@ -175,8 +177,7 @@ module OauthAuthentication current_user rescue => e - Rails.logger.error "Error linking GitHub account: #{e.message}" - Rails.logger.error e.backtrace.join("\n") + report_error(e, message: "Error linking GitHub account: #{e.message}") nil end end diff --git a/app/models/user.rb b/app/models/user.rb index ad9a101..b165c8d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -249,7 +249,7 @@ class User < ApplicationRecord if as_tz self.timezone = as_tz.name else - Rails.logger.error "Invalid timezone #{timezone} for user #{id}" + report_message("Invalid timezone #{timezone} for user #{id}") end end diff --git a/app/services/anonymize_user_service.rb b/app/services/anonymize_user_service.rb index 4b12a55..3d06482 100644 --- a/app/services/anonymize_user_service.rb +++ b/app/services/anonymize_user_service.rb @@ -1,4 +1,4 @@ -class AnonymizeUserService +class AnonymizeUserService < ApplicationService def self.call(user) new(user).call end @@ -14,8 +14,7 @@ class AnonymizeUserService destroy_associated_records end rescue StandardError => e - Sentry.capture_exception(e, extra: { user_id: user.id }) - Rails.logger.error "AnonymizeUserService failed for user #{user.id}: #{e.message}" + report_error(e, message: "AnonymizeUserService failed for user #{user.id}", extra: { user_id: user.id }) raise end diff --git a/app/services/application_service.rb b/app/services/application_service.rb new file mode 100644 index 0000000..3427082 --- /dev/null +++ b/app/services/application_service.rb @@ -0,0 +1,3 @@ +class ApplicationService + include ErrorReporting +end diff --git a/app/services/hack_club_geocoder_service.rb b/app/services/hack_club_geocoder_service.rb index 492e622..aee22d2 100644 --- a/app/services/hack_club_geocoder_service.rb +++ b/app/services/hack_club_geocoder_service.rb @@ -1,4 +1,4 @@ -class HackClubGeocoderService +class HackClubGeocoderService < ApplicationService BASE_URL = "https://geocoder.hackclub.com" def self.geoip(ip_address) @@ -43,11 +43,11 @@ class HackClubGeocoderService if response.is_a?(Net::HTTPSuccess) JSON.parse(response.body) else - Rails.logger.error "HackClub Geocoder API error: #{response.code} #{response.body}" + report_message("HackClub Geocoder API error: #{response.code} #{response.body}") nil end rescue => e - Rails.logger.error "HackClub Geocoder API request failed: #{e.message}" + report_error(e, message: "HackClub Geocoder API request failed") nil end end diff --git a/app/services/heartbeat_import_runner.rb b/app/services/heartbeat_import_runner.rb index 6fed739..7660a28 100644 --- a/app/services/heartbeat_import_runner.rb +++ b/app/services/heartbeat_import_runner.rb @@ -1,7 +1,11 @@ require "fileutils" require "zlib" -class HeartbeatImportRunner +class HeartbeatImportRunner < ApplicationService + class << self + include ErrorReporting + end + PROGRESS_INTERVAL = 250 REMOTE_REFRESH_THROTTLE = 5.seconds TMP_DIR = Rails.root.join("tmp", "heartbeat_imports") @@ -90,7 +94,7 @@ class HeartbeatImportRunner run.reload rescue => e - Rails.logger.error("Error refreshing heartbeat import run #{run&.id}: #{e.message}") + report_error(e, message: "Error refreshing heartbeat import run #{run&.id}") run end @@ -175,7 +179,7 @@ class HeartbeatImportRunner rescue => e run = HeartbeatImportRun.includes(:user).find_by(id: import_run_id) fail_run!(run, message: e.message) if run && !run.terminal? - Sentry.capture_exception(e) # Track unexpected errors + report_error(e, message: "HeartbeatImportDumpJob failed") # Track unexpected errors ensure FileUtils.rm_f(file_path) if file_path.present? ActiveRecord::Base.connection_pool.release_connection @@ -371,7 +375,7 @@ class HeartbeatImportRunner recipient_email: ).deliver_later rescue => e - Rails.logger.error("Failed to send heartbeat import completion email for run #{run.id}: #{e.message}") + report_error(e, message: "Failed to send heartbeat import completion email for run #{run.id}") end def self.send_failure_email(run) @@ -386,7 +390,7 @@ class HeartbeatImportRunner recipient_email: ).deliver_later rescue => e - Rails.logger.error("Failed to send heartbeat import failure email for run #{run.id}: #{e.message}") + report_error(e, message: "Failed to send heartbeat import failure email for run #{run.id}") end def self.send_import_email?(run) diff --git a/app/services/posthog_service.rb b/app/services/posthog_service.rb index 78d8db1..24d2a81 100644 --- a/app/services/posthog_service.rb +++ b/app/services/posthog_service.rb @@ -1,5 +1,6 @@ class PosthogService class << self + include ErrorReporting def capture(user_or_id, event, properties = {}) return unless $posthog @@ -11,7 +12,7 @@ class PosthogService properties: properties ) rescue => e - Rails.logger.error "PostHog capture error: #{e.message}" + report_error(e, message: "PostHog capture error") end def identify(user, properties = {}) @@ -29,7 +30,7 @@ class PosthogService }.merge(properties) ) rescue => e - Rails.logger.error "PostHog identify error: #{e.message}" + report_error(e, message: "PostHog identify error") end def capture_once_per_day(user, event, properties = {}) @@ -41,7 +42,7 @@ class PosthogService capture(user, event, properties) Rails.cache.write(cache_key, true, expires_at: Date.current.end_of_day + 1.hour) rescue => e - Rails.logger.error "PostHog capture_once_per_day error: #{e.message}" + report_error(e, message: "PostHog capture_once_per_day error") end end end diff --git a/app/services/repo_host/base_service.rb b/app/services/repo_host/base_service.rb index 6714161..7dcbd2b 100644 --- a/app/services/repo_host/base_service.rb +++ b/app/services/repo_host/base_service.rb @@ -1,5 +1,5 @@ module RepoHost - class BaseService + class BaseService < ApplicationService def initialize(user, repo_url) @user = user @repo_url = repo_url @@ -47,7 +47,7 @@ module RepoHost Rails.logger.warn "[#{self.class.name}] Repository #{owner}/#{repo} not found (404)" nil else - Rails.logger.error "[#{self.class.name}] API error. Status: #{response.status}" + report_message("[#{self.class.name}] API error. Status: #{response.status}") nil end end diff --git a/app/services/repo_host/github_service.rb b/app/services/repo_host/github_service.rb index 7cca67c..8f2d8c3 100644 --- a/app/services/repo_host/github_service.rb +++ b/app/services/repo_host/github_service.rb @@ -87,7 +87,7 @@ module RepoHost 0 end rescue => e - Rails.logger.error "[#{self.class.name}] Error fetching commit count for #{owner}/#{repo}: #{e.message}" + report_error(e, message: "[#{self.class.name}] Error fetching commit count for #{owner}/#{repo}") 0 end end diff --git a/config/initializers/posthog.rb b/config/initializers/posthog.rb index 174ee73..0e4f099 100644 --- a/config/initializers/posthog.rb +++ b/config/initializers/posthog.rb @@ -4,7 +4,7 @@ if ENV["POSTHOG_API_KEY"].present? $posthog = PostHog::Client.new({ api_key: ENV["POSTHOG_API_KEY"], host: ENV.fetch("POSTHOG_HOST", "https://us.i.posthog.com"), - on_error: proc { |status, msg| Rails.logger.error "PostHog error: #{status} - #{msg}" } + on_error: proc { |status, msg| Sentry.capture_message("PostHog error: #{status} - #{msg}"); Rails.logger.error "PostHog error: #{status} - #{msg}" } }) else $posthog = nil diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index b9b5e17..2455076 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -10,6 +10,7 @@ class Rack::Attack TOKENS = bypass_value.split(",").map(&:strip).reject(&:empty?).freeze Rails.logger.info "RACK_ATTACK_BYPASS loaded #{TOKENS.length} let me in tokens" rescue => e + Sentry.capture_exception(e) Rails.logger.error "RACK_ATTACK_BYPASS failed to read, you fucked it up #{e.message} raw: #{ENV['RACK_ATTACK_BYPASS'].inspect}" TOKENS = [].freeze end diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake index 2d08428..4822097 100644 --- a/lib/tasks/cache.rake +++ b/lib/tasks/cache.rake @@ -21,6 +21,7 @@ namespace :cache do puts "✓ #{job_class.name} completed" rescue => e puts "✗ #{job_class.name} failed: #{e.message}" + Sentry.capture_exception(e) Rails.logger.error("Cache warmup failed for #{job_class.name}: #{e.class.name} #{e.message}") end end diff --git a/lib/test_wakatime_service.rb b/lib/test_wakatime_service.rb index 8513e96..4a1ed47 100644 --- a/lib/test_wakatime_service.rb +++ b/lib/test_wakatime_service.rb @@ -1,4 +1,5 @@ include ApplicationHelper +include ErrorReporting class TestWakatimeService def initialize(user: nil, specific_filters: [], allow_cache: true, limit: 10, start_date: nil, end_date: nil, scope: nil, boundary_aware: false) @@ -123,7 +124,7 @@ class TestWakatimeService end end rescue => e - Rails.logger.error("Error parsing user agent string: #{e.message}") + report_error(e, message: "Error parsing user agent string") { os: "", editor: "", err: "failed to parse user agent string" } end @@ -161,7 +162,7 @@ class TestWakatimeService nil end rescue ArgumentError => e - Rails.logger.error("Error converting timestamp: #{e.message}") + report_error(e, message: "Error converting timestamp") nil end end diff --git a/lib/wakatime_service.rb b/lib/wakatime_service.rb index e5f708c..4d86256 100644 --- a/lib/wakatime_service.rb +++ b/lib/wakatime_service.rb @@ -1,4 +1,5 @@ include ApplicationHelper +include ErrorReporting class WakatimeService def initialize(user: nil, specific_filters: [], allow_cache: true, limit: 10, start_date: nil, end_date: nil, scope: nil) @@ -109,7 +110,7 @@ class WakatimeService end end rescue => e - Rails.logger.error("Error parsing user agent string: #{e.message}") + report_error(e, message: "Error parsing user agent string") { os: "", editor: "", err: "failed to parse user agent string" } end @@ -151,7 +152,7 @@ class WakatimeService nil end rescue ArgumentError => e - Rails.logger.error("Error converting timestamp: #{e.message}") + report_error(e, message: "Error converting timestamp") nil end end diff --git a/test/services/heartbeat_import_runner_test.rb b/test/services/heartbeat_import_runner_test.rb index aa4d22c..ef4a625 100644 --- a/test/services/heartbeat_import_runner_test.rb +++ b/test/services/heartbeat_import_runner_test.rb @@ -178,8 +178,6 @@ class HeartbeatImportRunnerTest < ActiveSupport::TestCase end test "refreshable_remote_run? stops once a remote import is downloading or importing" do - Flipper.enable_actor(:imports, User.first) - %i[downloading_dump importing].each do |state| user = User.create!(timezone: "UTC") Flipper.enable_actor(:imports, user)