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]) queue_project_mapping(heartbeat[:project])
results << [ new_heartbeat.attributes, 201 ] results << [ new_heartbeat.attributes, 201 ]
rescue => e rescue => e
Sentry.capture_exception(e) report_error(e, message: "Error creating heartbeat")
Rails.logger.error("Error creating heartbeat: #{e.class.name} #{e.message}")
results << [ { error: e.message, type: e.class.name }, 422 ] results << [ { error: e.message, type: e.class.name }, 422 ]
end end
@ -284,7 +283,7 @@ class Api::Hackatime::V1::HackatimeController < ApplicationController
end end
rescue => e rescue => e
# never raise an error here because it will break the heartbeat flow # 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 end
def check_lockout def check_lockout

View file

@ -53,8 +53,7 @@ module Api
render json: { error: user.errors.full_messages }, status: :unprocessable_entity render json: { error: user.errors.full_messages }, status: :unprocessable_entity
end end
rescue => e rescue => e
Sentry.capture_exception(e) report_error(e, message: "Error creating user from external Slack data")
Rails.logger.error "Error creating user from external Slack data: #{e.message}"
render json: { error: "Internal server error" }, status: :internal_server_error render json: { error: "Internal server error" }, status: :internal_server_error
end end

View file

@ -278,8 +278,7 @@ class Api::V1::StatsController < ApplicationController
JSON.parse(response.body)["user"]["id"] JSON.parse(response.body)["user"]["id"]
rescue => e rescue => e
Sentry.capture_exception(e) report_error(e, message: "Error finding user by email")
Rails.logger.error("Error finding user by email: #{e}")
nil nil
end end

View file

@ -1,4 +1,6 @@
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
include ErrorReporting
before_action :set_paper_trail_whodunnit before_action :set_paper_trail_whodunnit
before_action :sentry_context, if: :current_user before_action :sentry_context, if: :current_user
before_action :initialize_cache_counters 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) @deletion_request = DeletionRequest.create_for_user!(current_user)
redirect_to deletion_path redirect_to deletion_path
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
Sentry.capture_exception(e) report_error(e, message: "Deletion request creation failed")
redirect_to my_settings_path redirect_to my_settings_path
end end

View file

@ -97,7 +97,7 @@ class DocsController < InertiaController
format.md { render plain: content, content_type: "text/markdown" } format.md { render plain: content, content_type: "text/markdown" }
end end
rescue => e rescue => e
Rails.logger.error "Error loading docs: #{e.message}" report_error(e, message: "Error loading docs")
render_not_found render_not_found
end end

View file

@ -29,8 +29,7 @@ class My::HeartbeatImportsController < ApplicationController
rescue HeartbeatImportRunner::InvalidProviderError, ActionController::ParameterMissing => e rescue HeartbeatImportRunner::InvalidProviderError, ActionController::ParameterMissing => e
redirect_with_import_error(e.message) redirect_with_import_error(e.message)
rescue => e rescue => e
Sentry.capture_exception(e) report_error(e, message: "Error starting heartbeat import for user #{current_user&.id}")
Rails.logger.error("Error starting heartbeat import for user #{current_user&.id}: #{e.message}")
redirect_with_import_error("error reading file: #{e.message}") redirect_with_import_error("error reading file: #{e.message}")
end end

View file

@ -16,8 +16,7 @@ class SessionsController < ApplicationController
return return
end end
Rails.logger.error "HCA OAuth error: #{params[:error]}" report_message("HCA OAuth error: #{params[:error]}")
Sentry.capture_message("HCA OAuth error: #{params[:error]}")
redirect_to root_path, alert: "Failed to authenticate with Hack Club Auth. Error ID: #{Sentry.last_event_id}" redirect_to root_path, alert: "Failed to authenticate with Hack Club Auth. Error ID: #{Sentry.last_event_id}"
return return
end end
@ -70,8 +69,7 @@ class SessionsController < ApplicationController
return return
end end
Rails.logger.error "Slack OAuth error: #{params[:error]}" report_message("Slack OAuth error: #{params[:error]}")
Sentry.capture_message("Slack OAuth error: #{params[:error]}")
redirect_to root_path, alert: "Failed to authenticate with Slack. Error ID: #{Sentry.last_event_id}" redirect_to root_path, alert: "Failed to authenticate with Slack. Error ID: #{Sentry.last_event_id}"
return return
end end
@ -101,7 +99,7 @@ class SessionsController < ApplicationController
redirect_to root_path, notice: "Successfully signed in with Slack! Welcome!" redirect_to root_path, notice: "Successfully signed in with Slack! Welcome!"
end end
else 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" redirect_to root_path, alert: "Failed to sign in with Slack"
end end
end end
@ -133,8 +131,7 @@ class SessionsController < ApplicationController
redirect_uri = url_for(action: :github_create, only_path: false) redirect_uri = url_for(action: :github_create, only_path: false)
if params[:error].present? if params[:error].present?
Rails.logger.error "GitHub OAuth error: #{params[:error]}" report_message("GitHub OAuth error: #{params[:error]}")
Sentry.capture_message("GitHub OAuth error: #{params[:error]}")
redirect_to my_settings_path, alert: "Failed to authenticate with GitHub. Error ID: #{Sentry.last_event_id}" redirect_to my_settings_path, alert: "Failed to authenticate with GitHub. Error ID: #{Sentry.last_event_id}"
return return
end end
@ -150,7 +147,7 @@ class SessionsController < ApplicationController
PosthogService.capture(@user, "github_linked") PosthogService.capture(@user, "github_linked")
redirect_to my_settings_path, notice: "Successfully linked GitHub account!" redirect_to my_settings_path, notice: "Successfully linked GitHub account!"
else 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" redirect_to my_settings_path, alert: "Failed to link GitHub account"
end end
end end
@ -332,13 +329,13 @@ class SessionsController < ApplicationController
expected_nonce = session.delete(session_key) expected_nonce = session.delete(session_key)
if expected_nonce.blank? || received_nonce.blank? 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 return false
end end
return true if ActiveSupport::SecurityUtils.secure_compare(received_nonce.to_s, expected_nonce.to_s) 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 false
end end
end end

View file

@ -23,8 +23,7 @@ class Settings::AccessController < Settings::BaseController
render json: { token: new_api_key.token }, status: :ok render json: { token: new_api_key.token }, status: :ok
end end
rescue => e rescue => e
Sentry.capture_exception(e) report_error(e, message: "error rotate #{e.class.name}")
Rails.logger.error("error rotate #{e.class.name} #{e.message}")
render json: { error: "cant rotate" }, status: :unprocessable_entity render json: { error: "cant rotate" }, status: :unprocessable_entity
end end

View file

@ -17,8 +17,7 @@ class Settings::NotificationsController < Settings::BaseController
PosthogService.capture(@user, "settings_updated", { fields: [ "weekly_summary_email_enabled" ] }) PosthogService.capture(@user, "settings_updated", { fields: [ "weekly_summary_email_enabled" ] })
redirect_to my_settings_notifications_path, notice: "Settings updated successfully" redirect_to my_settings_notifications_path, notice: "Settings updated successfully"
rescue => e rescue => e
Sentry.capture_exception(e) report_error(e, message: "Failed to update notification settings")
Rails.logger.error("Failed to update notification settings: #{e.message}")
flash.now[:error] = "Failed to update settings, sorry :(" flash.now[:error] = "Failed to update settings, sorry :("
render_notifications(status: :unprocessable_entity) render_notifications(status: :unprocessable_entity)
end end

View file

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

View file

@ -62,7 +62,7 @@ class AttemptProjectRepoMappingJob < ApplicationJob
puts "repo: #{repo}" puts "repo: #{repo}"
repo["html_url"] repo["html_url"]
rescue JSON::ParserError => e 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 nil
end end
@ -79,7 +79,7 @@ class AttemptProjectRepoMappingJob < ApplicationJob
parsed_response parsed_response
rescue JSON::ParserError => e 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
end end

View file

@ -86,8 +86,7 @@ class GeocodeUsersWithoutCountryJob < ApplicationJob
return nil unless result&.country_code.present? return nil unless result&.country_code.present?
result.country_code.upcase result.country_code.upcase
rescue => e rescue => e
Rails.logger.error "geocode fail on #{ip}: #{e.message}" report_error(e, message: "geocode fail on #{ip}")
Sentry.capture_exception(e)
nil nil
end end
@ -98,8 +97,7 @@ class GeocodeUsersWithoutCountryJob < ApplicationJob
return nil unless tz&.tzinfo&.respond_to?(:country_code) return nil unless tz&.tzinfo&.respond_to?(:country_code)
tz.tzinfo.country_code&.upcase tz.tzinfo.country_code&.upcase
rescue => e rescue => e
Rails.logger.error "timezone geocode fail for #{timezone}: #{e.message}" report_error(e, message: "timezone geocode fail for #{timezone}")
Sentry.capture_exception(e)
nil nil
end end
end end

View file

@ -72,7 +72,7 @@ class HeartbeatExportJob < ApplicationJob
end end
end end
rescue ArgumentError => e 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 end
private private

View file

@ -7,7 +7,7 @@ class OneTime::SetUserTimezoneFromSlackJob < ApplicationJob
user.set_timezone_from_slack user.set_timezone_from_slack
user.save! user.save!
rescue => e 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 end
end end

View file

@ -11,8 +11,7 @@ class ProcessAccountDeletionsJob < ApplicationJob
Rails.logger.info "kerblamed account ##{deletion_request.user_id}" Rails.logger.info "kerblamed account ##{deletion_request.user_id}"
rescue StandardError => e rescue StandardError => e
Sentry.capture_exception(e, extra: { user_id: deletion_request.user_id }) report_error(e, message: "failed to kerblam ##{deletion_request.user_id}", extra: { user_id: deletion_request.user_id })
Rails.logger.error "failed to kerblam ##{deletion_request.user_id}: #{e.message}"
Rails.logger.error e.backtrace.join("\n") Rails.logger.error e.backtrace.join("\n")
end end
end end

View file

@ -37,7 +37,7 @@ class ProcessCommitJob < ApplicationJob
# when :gitlab # when :gitlab
# process_gitlab_commit(user, commit_sha, commit_api_url, repository) # process_gitlab_commit(user, commit_sha, commit_api_url, repository)
else 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
end end
@ -61,13 +61,13 @@ class ProcessCommitJob < ApplicationJob
api_commit_sha = commit_data_json["sha"] api_commit_sha = commit_data_json["sha"]
unless api_commit_sha == commit_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 return # Critical data integrity issue
end end
committer_date_str = commit_data_json.dig("commit", "committer", "date") committer_date_str = commit_data_json.dig("commit", "committer", "date")
unless committer_date_str 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 return
end end
@ -75,8 +75,8 @@ class ProcessCommitJob < ApplicationJob
# API dates are typically ISO8601 (UTC). Time.zone.parse respects the application's zone. # 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. # 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) commit_actual_created_at = Time.zone.parse(committer_date_str)
rescue ArgumentError rescue ArgumentError => e
Rails.logger.error "[ProcessCommitJob] Invalid committer date format '#{committer_date_str}' for commit #{commit_sha}." report_error(e, message: "[ProcessCommitJob] Invalid committer date format '#{committer_date_str}' for commit #{commit_sha}.")
return return
end end
@ -90,7 +90,7 @@ class ProcessCommitJob < ApplicationJob
Rails.logger.info "[ProcessCommitJob] Successfully processed commit #{api_commit_sha} for User ##{user.id}." Rails.logger.info "[ProcessCommitJob] Successfully processed commit #{api_commit_sha} for User ##{user.id}."
elsif response.status.code == 401 # Unauthorized 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) user.update!(github_access_token: nil)
Rails.logger.info "[ProcessCommitJob] Cleared invalid GitHub token for User ##{user.id}. User will need to re-authenticate." Rails.logger.info "[ProcessCommitJob] Cleared invalid GitHub token for User ##{user.id}. User will need to re-authenticate."
elsif response.status.code == 404 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}" 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) self.class.set(wait: delay_seconds.seconds).perform_later(user.id, commit_sha, commit_api_url, "github", repository&.id)
else 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 end
else 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 raise "GitHub API Error: Status #{response.status}" if response.status.server_error? # Trigger retry for server errors
end end
rescue HTTP::Error => e # Covers TimeoutError, ConnectionError 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 raise # Re-raise to allow GoodJob to retry based on retry_on
rescue JSON::ParserError => e 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. # Malformed JSON usually isn't temporary, so might not retry unless API is known to be flaky.
rescue ActiveRecord::RecordInvalid => e 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
end end

View file

@ -44,7 +44,7 @@ class PullRepoCommitsJob < ApplicationJob
process_commits(user, commits_data, repository) process_commits(user, commits_data, repository)
elsif response.status.code == 401 # Unauthorized 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) user.update!(github_access_token: nil)
Rails.logger.info "[PullRepoCommitsJob] Cleared invalid GitHub token for User ##{user.id}. User will need to re-authenticate." Rails.logger.info "[PullRepoCommitsJob] Cleared invalid GitHub token for User ##{user.id}. User will need to re-authenticate."
elsif response.status.code == 404 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." 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) self.class.set(wait: delay_seconds.seconds).perform_later(user.id, owner, repo)
else 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 end
else 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? raise "GitHub API Error: Status #{response.status}" if response.status.server_error?
end end
rescue HTTP::Error => e 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 raise # Re-raise to allow GoodJob to retry based on retry_on
rescue JSON::ParserError => e 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 raise # Re-raise to allow GoodJob to retry based on retry_on
end end
end end
@ -126,10 +126,10 @@ class PullRepoCommitsJob < ApplicationJob
Rails.logger.warn "[PullRepoCommitsJob] Failed to fetch commit details for #{commit_sha}: #{commit_response.status}" Rails.logger.warn "[PullRepoCommitsJob] Failed to fetch commit details for #{commit_sha}: #{commit_response.status}"
end end
rescue HTTP::Error => e 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 next
rescue JSON::ParserError => e 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 next
end end
end end

View file

@ -2,6 +2,8 @@ require "http" # Make sure 'http' gem is available
module RepoHost module RepoHost
class SyncUserEventsJob < ApplicationJob class SyncUserEventsJob < ApplicationJob
include ErrorReporting
queue_as :literally_whenever queue_as :literally_whenever
# MAX_API_PAGES_TO_FETCH: Max pages to fetch. GitHub's /users/{username}/events endpoint # MAX_API_PAGES_TO_FETCH: Max pages to fetch. GitHub's /users/{username}/events endpoint
@ -38,7 +40,7 @@ module RepoHost
# when :gitlab # when :gitlab
# process_gitlab_events # process_gitlab_events
else 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 return
end end
Rails.logger.info "Finished event sync for User ##{@user.id}, Provider: #{@provider_sym}." Rails.logger.info "Finished event sync for User ##{@user.id}, Provider: #{@provider_sym}."
@ -69,7 +71,7 @@ module RepoHost
begin begin
response = http_client_for_github.get(api_url) response = http_client_for_github.get(api_url)
rescue HTTP::Error => e 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 break
end end
@ -141,7 +143,7 @@ module RepoHost
def handle_github_api_error(response, page_number) def handle_github_api_error(response, page_number)
error_details = response.parse rescue response.body.to_s.truncate(255) 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}" 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 case response.status.code
when 401 # Unauthorized when 401 # Unauthorized
@ -158,7 +160,7 @@ module RepoHost
when 422 # Unprocessable Entity - often if the user has been suspended 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}" Rails.logger.warn "GitHub API returned 422 for User ##{@user.id}. User might be suspended. Sync aborted. Details: #{error_details}"
else 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 end
end end

View file

@ -48,7 +48,7 @@ class SailorsLogNotifyJob < ApplicationJob
slsn.update(sent: true) slsn.update(sent: true)
SailorsLogTeletypeJob.perform_later(message) SailorsLogTeletypeJob.perform_later(message)
else 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] ignorable_errors = %w[channel_not_found is_archived]
if ignorable_errors.include?(response_data["error"]) if ignorable_errors.include?(response_data["error"])
# disable any preferences for this channel # disable any preferences for this channel

View file

@ -79,9 +79,9 @@ class ScanRepoEventsForCommitsJob < ApplicationJob
potential_commits_buffer.clear potential_commits_buffer.clear
end end
rescue JSON::ParserError => e 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 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 end
# Process any remaining commits in the buffer # Process any remaining commits in the buffer

View file

@ -29,8 +29,7 @@ class SetUserCountryCodeJob < ApplicationJob
user.update!(country_code: country_code) user.update!(country_code: country_code)
end end
rescue => e rescue => e
Rails.logger.error "timezone geocode fail for #{@user.timezone}: #{e.message}" report_error(e, message: "timezone geocode fail for #{@user.timezone}")
Sentry.capture_exception(e)
end end
end end
@ -53,8 +52,7 @@ class SetUserCountryCodeJob < ApplicationJob
result.country_code.upcase result.country_code.upcase
rescue => e rescue => e
Rails.logger.error "geocode fail on #{ip}: #{e.message}" report_error(e, message: "geocode fail on #{ip}")
Sentry.capture_exception(e)
end end
end end
end end

View file

@ -19,7 +19,7 @@ class SlackUsernameUpdateJob < ApplicationJob
user.update_from_slack user.update_from_slack
user.save! user.save!
rescue => e 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 end
end end

View file

@ -36,7 +36,7 @@ class SyncRepoMetadataJob < ApplicationJob
raise raise
end end
rescue => e rescue => e
Rails.logger.error "[SyncRepoMetadataJob] Unexpected error: #{e.message}" report_error(e, message: "[SyncRepoMetadataJob] Unexpected error")
raise # Retry for other errors raise # Retry for other errors
end end
end end

View file

@ -32,7 +32,7 @@ class SyncStaleRepoMetadataJob < ApplicationJob
mapping.update!(repository: repo) mapping.update!(repository: repo)
repos_to_sync[repo.id] = repo repos_to_sync[repo.id] = repo
rescue => e 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 end
end end

View file

@ -8,7 +8,7 @@ class UserSlackStatusUpdateJob < ApplicationJob
begin begin
user.update_slack_status user.update_slack_status
rescue => e 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 end
end end

View file

@ -1,7 +1,10 @@
module OauthAuthentication module OauthAuthentication
extend ActiveSupport::Concern extend ActiveSupport::Concern
include ErrorReporting
class_methods do class_methods do
include ErrorReporting
def hca_authorize_url(redirect_uri) def hca_authorize_url(redirect_uri)
params = { params = {
redirect_uri:, redirect_uri:,
@ -132,8 +135,7 @@ module OauthAuthentication
user.save! user.save!
user user
rescue => e rescue => e
Rails.logger.error "Error creating user from Slack data: #{e.message}" report_error(e, message: "Error creating user from Slack data: #{e.message}")
Rails.logger.error e.backtrace.join("\n")
nil nil
end end
@ -175,8 +177,7 @@ module OauthAuthentication
current_user current_user
rescue => e rescue => e
Rails.logger.error "Error linking GitHub account: #{e.message}" report_error(e, message: "Error linking GitHub account: #{e.message}")
Rails.logger.error e.backtrace.join("\n")
nil nil
end end
end end

View file

@ -249,7 +249,7 @@ class User < ApplicationRecord
if as_tz if as_tz
self.timezone = as_tz.name self.timezone = as_tz.name
else else
Rails.logger.error "Invalid timezone #{timezone} for user #{id}" report_message("Invalid timezone #{timezone} for user #{id}")
end end
end end

View file

@ -1,4 +1,4 @@
class AnonymizeUserService class AnonymizeUserService < ApplicationService
def self.call(user) def self.call(user)
new(user).call new(user).call
end end
@ -14,8 +14,7 @@ class AnonymizeUserService
destroy_associated_records destroy_associated_records
end end
rescue StandardError => e rescue StandardError => e
Sentry.capture_exception(e, extra: { user_id: user.id }) report_error(e, message: "AnonymizeUserService failed for user #{user.id}", extra: { user_id: user.id })
Rails.logger.error "AnonymizeUserService failed for user #{user.id}: #{e.message}"
raise raise
end 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" BASE_URL = "https://geocoder.hackclub.com"
def self.geoip(ip_address) def self.geoip(ip_address)
@ -43,11 +43,11 @@ class HackClubGeocoderService
if response.is_a?(Net::HTTPSuccess) if response.is_a?(Net::HTTPSuccess)
JSON.parse(response.body) JSON.parse(response.body)
else else
Rails.logger.error "HackClub Geocoder API error: #{response.code} #{response.body}" report_message("HackClub Geocoder API error: #{response.code} #{response.body}")
nil nil
end end
rescue => e rescue => e
Rails.logger.error "HackClub Geocoder API request failed: #{e.message}" report_error(e, message: "HackClub Geocoder API request failed")
nil nil
end end
end end

View file

@ -1,7 +1,11 @@
require "fileutils" require "fileutils"
require "zlib" require "zlib"
class HeartbeatImportRunner class HeartbeatImportRunner < ApplicationService
class << self
include ErrorReporting
end
PROGRESS_INTERVAL = 250 PROGRESS_INTERVAL = 250
REMOTE_REFRESH_THROTTLE = 5.seconds REMOTE_REFRESH_THROTTLE = 5.seconds
TMP_DIR = Rails.root.join("tmp", "heartbeat_imports") TMP_DIR = Rails.root.join("tmp", "heartbeat_imports")
@ -90,7 +94,7 @@ class HeartbeatImportRunner
run.reload run.reload
rescue => e 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 run
end end
@ -175,7 +179,7 @@ class HeartbeatImportRunner
rescue => e rescue => e
run = HeartbeatImportRun.includes(:user).find_by(id: import_run_id) run = HeartbeatImportRun.includes(:user).find_by(id: import_run_id)
fail_run!(run, message: e.message) if run && !run.terminal? 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 ensure
FileUtils.rm_f(file_path) if file_path.present? FileUtils.rm_f(file_path) if file_path.present?
ActiveRecord::Base.connection_pool.release_connection ActiveRecord::Base.connection_pool.release_connection
@ -371,7 +375,7 @@ class HeartbeatImportRunner
recipient_email: recipient_email:
).deliver_later ).deliver_later
rescue => e 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 end
def self.send_failure_email(run) def self.send_failure_email(run)
@ -386,7 +390,7 @@ class HeartbeatImportRunner
recipient_email: recipient_email:
).deliver_later ).deliver_later
rescue => e 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 end
def self.send_import_email?(run) def self.send_import_email?(run)

View file

@ -1,5 +1,6 @@
class PosthogService class PosthogService
class << self class << self
include ErrorReporting
def capture(user_or_id, event, properties = {}) def capture(user_or_id, event, properties = {})
return unless $posthog return unless $posthog
@ -11,7 +12,7 @@ class PosthogService
properties: properties properties: properties
) )
rescue => e rescue => e
Rails.logger.error "PostHog capture error: #{e.message}" report_error(e, message: "PostHog capture error")
end end
def identify(user, properties = {}) def identify(user, properties = {})
@ -29,7 +30,7 @@ class PosthogService
}.merge(properties) }.merge(properties)
) )
rescue => e rescue => e
Rails.logger.error "PostHog identify error: #{e.message}" report_error(e, message: "PostHog identify error")
end end
def capture_once_per_day(user, event, properties = {}) def capture_once_per_day(user, event, properties = {})
@ -41,7 +42,7 @@ class PosthogService
capture(user, event, properties) capture(user, event, properties)
Rails.cache.write(cache_key, true, expires_at: Date.current.end_of_day + 1.hour) Rails.cache.write(cache_key, true, expires_at: Date.current.end_of_day + 1.hour)
rescue => e 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 end
end end

View file

@ -1,5 +1,5 @@
module RepoHost module RepoHost
class BaseService class BaseService < ApplicationService
def initialize(user, repo_url) def initialize(user, repo_url)
@user = user @user = user
@repo_url = repo_url @repo_url = repo_url
@ -47,7 +47,7 @@ module RepoHost
Rails.logger.warn "[#{self.class.name}] Repository #{owner}/#{repo} not found (404)" Rails.logger.warn "[#{self.class.name}] Repository #{owner}/#{repo} not found (404)"
nil nil
else else
Rails.logger.error "[#{self.class.name}] API error. Status: #{response.status}" report_message("[#{self.class.name}] API error. Status: #{response.status}")
nil nil
end end
end end

View file

@ -87,7 +87,7 @@ module RepoHost
0 0
end end
rescue => e 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 0
end end
end end

View file

@ -4,7 +4,7 @@ if ENV["POSTHOG_API_KEY"].present?
$posthog = PostHog::Client.new({ $posthog = PostHog::Client.new({
api_key: ENV["POSTHOG_API_KEY"], api_key: ENV["POSTHOG_API_KEY"],
host: ENV.fetch("POSTHOG_HOST", "https://us.i.posthog.com"), 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 else
$posthog = nil $posthog = nil

View file

@ -10,6 +10,7 @@ class Rack::Attack
TOKENS = bypass_value.split(",").map(&:strip).reject(&:empty?).freeze TOKENS = bypass_value.split(",").map(&:strip).reject(&:empty?).freeze
Rails.logger.info "RACK_ATTACK_BYPASS loaded #{TOKENS.length} let me in tokens" Rails.logger.info "RACK_ATTACK_BYPASS loaded #{TOKENS.length} let me in tokens"
rescue => e 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}" Rails.logger.error "RACK_ATTACK_BYPASS failed to read, you fucked it up #{e.message} raw: #{ENV['RACK_ATTACK_BYPASS'].inspect}"
TOKENS = [].freeze TOKENS = [].freeze
end end

View file

@ -21,6 +21,7 @@ namespace :cache do
puts "#{job_class.name} completed" puts "#{job_class.name} completed"
rescue => e rescue => e
puts "#{job_class.name} failed: #{e.message}" 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}") Rails.logger.error("Cache warmup failed for #{job_class.name}: #{e.class.name} #{e.message}")
end end
end end

View file

@ -1,4 +1,5 @@
include ApplicationHelper include ApplicationHelper
include ErrorReporting
class TestWakatimeService class TestWakatimeService
def initialize(user: nil, specific_filters: [], allow_cache: true, limit: 10, start_date: nil, end_date: nil, scope: nil, boundary_aware: false) 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
end end
rescue => e 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" } { os: "", editor: "", err: "failed to parse user agent string" }
end end
@ -161,7 +162,7 @@ class TestWakatimeService
nil nil
end end
rescue ArgumentError => e rescue ArgumentError => e
Rails.logger.error("Error converting timestamp: #{e.message}") report_error(e, message: "Error converting timestamp")
nil nil
end end
end end

View file

@ -1,4 +1,5 @@
include ApplicationHelper include ApplicationHelper
include ErrorReporting
class WakatimeService class WakatimeService
def initialize(user: nil, specific_filters: [], allow_cache: true, limit: 10, start_date: nil, end_date: nil, scope: nil) 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
end end
rescue => e 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" } { os: "", editor: "", err: "failed to parse user agent string" }
end end
@ -151,7 +152,7 @@ class WakatimeService
nil nil
end end
rescue ArgumentError => e rescue ArgumentError => e
Rails.logger.error("Error converting timestamp: #{e.message}") report_error(e, message: "Error converting timestamp")
nil nil
end end
end end

View file

@ -178,8 +178,6 @@ class HeartbeatImportRunnerTest < ActiveSupport::TestCase
end end
test "refreshable_remote_run? stops once a remote import is downloading or importing" do 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| %i[downloading_dump importing].each do |state|
user = User.create!(timezone: "UTC") user = User.create!(timezone: "UTC")
Flipper.enable_actor(:imports, user) Flipper.enable_actor(:imports, user)