Skip to content

Commit 2b1317d

Browse files
committed
Improve bugsnag notification in jobs
1 parent f62e295 commit 2b1317d

8 files changed

Lines changed: 258 additions & 133 deletions

File tree

app/jobs/application_job.rb

Lines changed: 97 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,105 @@ class ApplicationJob < ActiveJob::Base
55
# Most jobs are safe to ignore if the underlying records are no longer available
66
discard_on ActiveJob::DeserializationError
77

8-
# Global error handling
8+
# Handle record not found errors - log and notify but don't fail the job
9+
rescue_from ActiveRecord::RecordNotFound do |exception|
10+
handle_error(exception, action: 'record_not_found', severity: :warn, reraise: false)
11+
end
12+
13+
# Handle record validation errors - log validation details
14+
rescue_from ActiveRecord::RecordInvalid do |exception|
15+
metadata = {
16+
action: 'record_invalid',
17+
record_errors: exception.record&.errors&.full_messages&.join(', ')
18+
}
19+
handle_error(exception, metadata: metadata, severity: :error, reraise: false)
20+
end
21+
22+
# Handle all other standard errors
923
rescue_from StandardError do |exception|
24+
handle_error(exception, action: 'job_failed', severity: :error, reraise: true)
25+
end
26+
27+
private
28+
29+
# Unified error handling with Bugsnag notification and logging
30+
def handle_error(exception, metadata: {}, action: nil, severity: :error, reraise: true)
31+
# Build metadata for Bugsnag and logging
32+
error_metadata = build_error_metadata(metadata.merge(action: action))
33+
34+
# Send to Bugsnag
35+
Bugsnag.notify(exception) do |b|
36+
b.metadata = error_metadata
37+
b.severity = severity
38+
end
39+
1040
# Log the error with context
11-
Rails.logger.error(
12-
"Job failed: #{self.class.name}",
41+
log_error(exception, error_metadata, severity)
42+
43+
# Re-raise if requested to allow retry logic to work
44+
raise exception if reraise
45+
end
46+
47+
# Build base error metadata with job context
48+
def build_error_metadata(additional_metadata = {})
49+
base_metadata = {
50+
job_class: self.class.name,
1351
job_id: job_id,
14-
arguments: arguments,
15-
error: exception.class.name,
16-
message: exception.message,
17-
backtrace: exception.backtrace.first(5)
18-
)
19-
20-
# Re-raise to allow retry logic to work
21-
raise exception
52+
arguments: sanitized_arguments,
53+
error_class: exception_class_name(additional_metadata[:exception])
54+
}
55+
56+
base_metadata.merge(additional_metadata.compact)
57+
end
58+
59+
# Sanitize arguments for logging (remove sensitive data)
60+
def sanitized_arguments
61+
return [] unless arguments.respond_to?(:map)
62+
63+
arguments.map do |arg|
64+
case arg
65+
when String
66+
arg.length > 100 ? "#{arg[0..97]}..." : arg
67+
when Hash
68+
arg.slice(:id, :user_id, :channel_id, :post_id, :action)
69+
else
70+
arg.class.name
71+
end
72+
end
73+
end
74+
75+
# Extract exception class name safely
76+
def exception_class_name(exception)
77+
exception&.class&.name || 'Unknown'
78+
end
79+
80+
# Log error with appropriate level and context
81+
def log_error(exception, metadata, severity)
82+
log_message = "#{severity.to_s.capitalize} in #{metadata[:job_class]}: #{exception.message}"
83+
84+
case severity
85+
when :debug
86+
Rails.logger.debug(log_message, metadata)
87+
when :info
88+
Rails.logger.info(log_message, metadata)
89+
when :warn
90+
Rails.logger.warn(log_message, metadata)
91+
else
92+
Rails.logger.error(log_message, metadata)
93+
Rails.logger.error(exception.backtrace&.first(10)&.join("\n"))
94+
end
95+
end
96+
97+
# Helper method to add context to error handling
98+
def with_error_context(context)
99+
@error_context = context
100+
yield
101+
ensure
102+
@error_context = nil
103+
end
104+
105+
# Get current error context if set
106+
def current_error_context
107+
@error_context || {}
22108
end
23109
end

app/jobs/channels/fetch_posts_job.rb

Lines changed: 54 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,62 +5,71 @@ class Channels::FetchPostsJob < ApplicationJob
55
retry_on StandardError, wait: 1.minute, attempts: 3
66

77
def perform(channel_id)
8-
channel = Channel.find(channel_id)
8+
with_error_context(channel_id: channel_id, action: 'fetch_posts') do
9+
channel = Channel.find(channel_id)
910

10-
Rails.logger.info "Fetching posts for channel #{channel.username}"
11+
Rails.logger.info "Fetching posts for channel #{channel.username}"
1112

12-
# Получаем Telegram ID канала
13-
chat_id = channel.telegram_id.to_s
13+
# Получаем Telegram ID канала
14+
chat_id = channel.telegram_id.to_s
1415

15-
# Инициализируем клиент
16-
bot = Telegram.bots[:default]
17-
fetcher = TelegramClient::ChannelFetcher.new(bot)
16+
# Инициализируем клиент
17+
bot = Telegram.bots[:default]
18+
fetcher = TelegramClient::ChannelFetcher.new(bot)
1819

19-
# Проверяем доступность канала
20-
unless fetcher.channel_available?(channel.username)
21-
Rails.logger.warn "Channel #{channel.username} is not available, deactivating"
22-
channel.deactivate!
23-
return
24-
end
20+
# Проверяем доступность канала
21+
unless fetcher.channel_available?(channel.username)
22+
Rails.logger.warn "Channel #{channel.username} is not available, deactivating"
23+
channel.deactivate!
24+
return
25+
end
2526

26-
# Получаем последние посты
27-
posts_data = fetcher.get_channel_posts(channel.username, limit: 20)
27+
# Получаем последние посты
28+
posts_data = fetcher.get_channel_posts(channel.username, limit: 20)
2829

29-
Rails.logger.info "Fetched #{posts_data.count} posts from channel #{channel.username}"
30+
Rails.logger.info "Fetched #{posts_data.count} posts from channel #{channel.username}"
3031

31-
# Обновляем время последнего поста если есть посты
32-
if posts_data.any?
33-
channel.update_last_post!
34-
end
32+
# Обновляем время последнего поста если есть посты
33+
if posts_data.any?
34+
channel.update_last_post!
35+
end
3536

36-
# Обрабатываем каждый пост
37-
posts_data.each do |post_data|
38-
begin
39-
# Проверяем, нет ли уже такого поста в БД
40-
existing_post = Post.find_by(
41-
channel: channel,
42-
telegram_message_id: post_data[:telegram_message_id]
43-
)
44-
45-
if existing_post
46-
Rails.logger.debug "Post #{post_data[:telegram_message_id]} already exists, skipping"
47-
next
48-
end
49-
50-
# Запускаем задачу для сохранения и обработки поста
51-
Content::ProcessPostJob.perform_later(channel.id, post_data)
52-
53-
Rails.logger.debug "Scheduled processing for post #{post_data[:telegram_message_id]}"
54-
rescue StandardError => e
55-
Rails.logger.error "Error processing post #{post_data[:telegram_message_id]}: #{e.message}"
37+
# Обрабатываем каждый пост
38+
posts_data.each do |post_data|
39+
process_single_post(channel, post_data)
5640
end
41+
42+
Rails.logger.info "Completed fetching posts for channel #{channel.username}"
5743
end
44+
end
45+
46+
private
47+
48+
def process_single_post(channel, post_data)
49+
# Проверяем, нет ли уже такого поста в БД
50+
existing_post = Post.find_by(
51+
channel: channel,
52+
telegram_message_id: post_data[:telegram_message_id]
53+
)
54+
55+
if existing_post
56+
Rails.logger.debug "Post #{post_data[:telegram_message_id]} already exists, skipping"
57+
return
58+
end
59+
60+
# Запускаем задачу для сохранения и обработки поста
61+
Content::ProcessPostJob.perform_later(channel.id, post_data)
5862

59-
Rails.logger.info "Completed fetching posts for channel #{channel.username}"
60-
rescue ActiveRecord::RecordNotFound => e
61-
Rails.logger.error "Channel #{channel_id} not found: #{e.message}"
63+
Rails.logger.debug "Scheduled processing for post #{post_data[:telegram_message_id]}"
6264
rescue StandardError => e
63-
Rails.logger.error "Error fetching posts for channel #{channel_id}: #{e.message}"
64-
raise
65+
# Handle individual post processing errors without failing the entire job
66+
handle_error(e,
67+
metadata: {
68+
channel_id: channel.id,
69+
message_id: post_data[:telegram_message_id],
70+
action: 'process_single_post'
71+
},
72+
severity: :warn,
73+
reraise: false)
6574
end
6675
end

app/jobs/channels/monitor_job.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,14 @@ def perform(*args)
1818
Rails.logger.info "Found #{channels.count} channels to monitor"
1919

2020
channels.each do |channel|
21-
begin
21+
with_error_context(channel_id: channel.id, channel_username: channel.username) do
2222
# Запускаем задачу для получения постов из канала
2323
Channels::FetchPostsJob.perform_later(channel.id)
2424

2525
# Обновляем время последней проверки
2626
channel.mark_as_monitored!
2727

2828
Rails.logger.debug "Scheduled fetch job for channel #{channel.username}"
29-
rescue StandardError => e
30-
Rails.logger.error "Error scheduling fetch for channel #{channel.username}: #{e.message}"
3129
end
3230
end
3331

app/jobs/content/deliver_posts_job.rb

Lines changed: 60 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,51 +5,69 @@ class Content::DeliverPostsJob < ApplicationJob
55
retry_on StandardError, wait: 30.seconds, attempts: 3
66

77
def perform(user_id, post_ids)
8-
user = TelegramUser.find(user_id)
9-
posts = Post.where(id: post_ids)
10-
11-
Rails.logger.info "Delivering #{posts.count} posts to user #{user.username}"
12-
13-
# Получаем инстанс бота
14-
bot = Telegram.bots[:default]
15-
16-
posts.each do |post|
17-
begin
18-
# Форвардим сообщение пользователю
19-
response = bot.api.forward_message(
20-
chat_id: user.telegram_id, # chat_id пользователя
21-
from_chat_id: post.channel.telegram_id, # ID канала
22-
message_id: post.telegram_message_id # ID сообщения в канале
23-
)
24-
25-
if response['ok']
26-
Rails.logger.debug "Successfully forwarded post #{post.id} to user #{user.username}"
27-
else
28-
Rails.logger.error "Failed to forward post #{post.id} to user #{user.username}: #{response["description"]}"
29-
end
30-
31-
# Небольшая задержка между сообщениями чтобы избежать rate limits
32-
sleep(0.1)
33-
34-
rescue StandardError => e
35-
Rails.logger.error "Error forwarding post #{post.id} to user #{user.username}: #{e.message}"
36-
37-
# Если ошибка связана с отсутствием прав или удаленным постом, логируем и продолжаем
38-
if e.message.include?('Bad Request') || e.message.include?('not found')
39-
Rails.logger.warn "Skipping post #{post.id} due to access restrictions or deletion"
40-
next
41-
else
42-
# Для других ошибок пробуем еще раз (retry_on обработает)
43-
raise
44-
end
8+
with_error_context(user_id: user_id, post_ids: post_ids, action: 'deliver_posts') do
9+
user = TelegramUser.find(user_id)
10+
posts = Post.where(id: post_ids)
11+
12+
Rails.logger.info "Delivering #{posts.count} posts to user #{user.username}"
13+
14+
# Получаем инстанс бота
15+
bot = Telegram.bots[:default]
16+
17+
posts.each do |post|
18+
deliver_single_post(user, post, bot)
4519
end
20+
21+
Rails.logger.info "Completed delivery to user #{user.username}"
4622
end
23+
end
24+
25+
private
26+
27+
def deliver_single_post(user, post, bot)
28+
# Форвардим сообщение пользователю
29+
response = bot.api.forward_message(
30+
chat_id: user.telegram_id, # chat_id пользователя
31+
from_chat_id: post.channel.telegram_id, # ID канала
32+
message_id: post.telegram_message_id # ID сообщения в канале
33+
)
4734

48-
Rails.logger.info "Completed delivery to user #{user.username}"
49-
rescue ActiveRecord::RecordNotFound => e
50-
Rails.logger.error "User #{user_id} or posts not found: #{e.message}"
35+
if response['ok']
36+
Rails.logger.debug "Successfully forwarded post #{post.id} to user #{user.username}"
37+
else
38+
Rails.logger.error "Failed to forward post #{post.id} to user #{user.username}: #{response["description"]}"
39+
end
40+
41+
# Небольшая задержка между сообщениями чтобы избежать rate limits
42+
sleep(0.1)
5143
rescue StandardError => e
52-
Rails.logger.error "Error delivering posts to user #{user_id}: #{e.message}"
53-
raise
44+
# Handle individual post delivery errors
45+
handle_post_delivery_error(e, user, post)
46+
end
47+
48+
def handle_post_delivery_error(error, user, post)
49+
# Если ошибка связана с отсутствием прав или удаленным постом, логируем и продолжаем
50+
if error.message.include?('Bad Request') || error.message.include?('not found')
51+
Rails.logger.warn "Skipping post #{post.id} due to access restrictions or deletion"
52+
handle_error(error,
53+
metadata: {
54+
user_id: user.id,
55+
post_id: post.id,
56+
action: 'skip_post_delivery'
57+
},
58+
severity: :warn,
59+
reraise: false)
60+
return
61+
end
62+
63+
# Для других ошибок пробуем еще раз (retry_on обработает)
64+
handle_error(error,
65+
metadata: {
66+
user_id: user.id,
67+
post_id: post.id,
68+
action: 'forward_post'
69+
},
70+
severity: :error,
71+
reraise: true)
5472
end
5573
end

0 commit comments

Comments
 (0)