Add Sentry monitoring for previously unreported errors (#1066)

* Add Sentry monitoring for previously unreported errors

* Fix

* Fixes

* whoops!
This commit is contained in:
Mahad Kalam 2026-03-13 11:06:12 +00:00 committed by GitHub
parent 922e7384c0
commit 28fa174861
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 118 additions and 97 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,6 @@
class ApplicationJob < ActiveJob::Base
include ErrorReporting
# Automatically retry jobs that encountered a deadlock
# retry_on ActiveRecord::Deadlocked

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,3 @@
class ApplicationService
include ErrorReporting
end

View file

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

View file

@ -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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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)