tg_error_notifier
Gem for Rails error notifications to Telegram.
What it catches
- Unhandled errors in Rack/Rails request cycle.
- Failed ActiveJob executions.
Quick usage
# Gemfile
gem "tg_error_notifier"# config/initializers/telegram_error_notifier.rb
Rails.application.configure do
config.telegram_error_notifier.bot_token = ENV["TELEGRAM_BOT_TOKEN"]
config.telegram_error_notifier.chat_id = ENV["TELEGRAM_ERRORS_CHAT_ID"]
config.telegram_error_notifier.app_name = "my_app"
endEnvironment variables
TELEGRAM_BOT_TOKENTELEGRAM_ERRORS_CHAT_ID-
TELEGRAM_ERRORS_APP_NAME(optional)
Telegram setup (bot, private channel/group, chat_id)
1. Create a bot and get token
- Open Telegram and start chat with
@BotFather. - Send
/newbotand follow prompts (bot name + username ending withbot). - Copy the HTTP API token (looks like
123456:ABC...) and save it asTELEGRAM_BOT_TOKEN.
2. Create a private channel or private group
Private channel
- Telegram -> New Channel -> set as Private.
- Open channel settings -> Administrators -> add your bot as admin.
- Grant at least permission to post messages.
Private group (or supergroup)
- Telegram -> New Group -> add at least one member.
- Add your bot to the group.
- Promote bot to admin if needed (recommended for reliability).
- Keep group private (no public username).
3. Get chat_id
Method A (easy): @userinfobot / @RawDataBot
- Add bot
@userinfobot(or@RawDataBot) into your target channel/group. - Send any message there.
- Open the helper bot dialog and read chat id.
- For channels/supergroups it is usually negative and starts with
-100.... - Delete this bot from your channel.
Method B (official API): getUpdates
- Send at least one message in target chat after adding your bot.
- Open in browser:
https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/getUpdates - Find
chatobject and copychat.id.
Examples:
- Private channel/supergroup:
-1001234567890 - Private group (old style):
-123456789
4. Validate before using in Rails
- Ensure bot is present in the target chat.
- Ensure bot has permission to send messages.
- Put value into
TELEGRAM_ERRORS_CHAT_IDand run a smoke test.
Error grouping
Group identical errors to avoid flooding. When the same exception repeats within a time window, only the first message is sent — subsequent occurrences are suppressed and reported as a count in the next message.
TgErrorNotifier.configure do |config|
config.grouping_enabled = true
config.grouping_window = 60 # seconds (default)
endErrors are grouped by exception class + normalized message (IDs and UUIDs are replaced with placeholders for better deduplication).
Forum topics (threads)
Automatically create a Telegram Forum topic (thread) per unique error type. Each error gets its own topic in a supergroup with Forum Topics enabled.
TgErrorNotifier.configure do |config|
config.topics_enabled = true
config.topic_icon_color = 0xFB6F5F # red (default), optional
endRequirements: The chat must be a supergroup with Forum Topics enabled. The bot must have can_manage_topics admin permission.
You can combine both features — errors will be grouped within their respective topics:
TgErrorNotifier.configure do |config|
config.grouping_enabled = true
config.grouping_window = 60
config.topics_enabled = true
endManual notification
begin
do_work
rescue => e
pp TgErrorNotifier.capture_exception(e)
# or with extra context:
TgErrorNotifier.capture_exception(
e,
source: "custom",
context: { feature: "sync", user_id: current_user&.id }
)
raise
endcapture_exception returns a diagnostic hash, e.g.:
{ sent: true, status: :sent, code: 200 }{ sent: false, status: :skipped, reason: "missing_chat_id" }{ sent: false, status: :failed, reason: "telegram_api_error", code: 400, body: "..." }
Manual message
TgErrorNotifier.capture_message(
"Background sync started",
level: :info,
source: "custom",
context: { feature: "sync", user_id: current_user&.id }
)capture_message returns the same diagnostic hash format as capture_exception.
Named topics for messages
Send messages to a dedicated Forum topic by name. The topic is created on first use (blue icon) and reused afterwards:
TgErrorNotifier.capture_message("New account: #{account.name}", topic: "Registrations")Since the topic → thread_id mapping is cached in memory per process, configure a persistent store so restarts and multiple processes (Puma workers, Sidekiq) don't create duplicate topics:
TgErrorNotifier.configure do |config|
config.topic_store_read = ->(name) { MyKeyValueStore.get("tg_topic:#{name}") }
config.topic_store_write = ->(name, thread_id) { MyKeyValueStore.set("tg_topic:#{name}", thread_id) }
endWithout a store everything still works, but each process creates its own topic. Named topics work independently of topics_enabled (which controls per-exception topics). Requirements are the same: forum supergroup + can_manage_topics bot permission.
Filtering request data
The query string of a failed request is filtered with Rails.application.config.filter_parameters (the same rules as the Rails log) before it is sent: ?token=abc arrives as ?token=[FILTERED]. A query that cannot be parsed is not sent at all. Override the rules with config.filter_parameters = [...].
Paths that carry a secret as a segment (one-time login links, invitations) are masked by prefix:
TgErrorNotifier.configure do |config|
config.sensitive_path_prefixes = %w[/tsd/ /invitations/]
end
# "/tsd/abc123/scan" is reported as "/tsd/[FILTERED]/scan"Buttons under the message
Attach an inline keyboard to error notifications — e.g. a link that files the error as a task on your board. The gem does not interpret the buttons: whatever the callable returns is passed to Telegram as is, so url, callback_data and web_app buttons all work.
TgErrorNotifier.configure do |config|
config.buttons = lambda do |kind:, source:, context:, fingerprint:, exception: nil, message: nil|
next nil unless kind == :exception
token = ErrorTaskLink.issue(exception: exception, source: source, fingerprint: fingerprint)
[[{ text: "\u2795 To the board", url: "https://example.com/e/#{token}" }]]
end
endArguments:
| key | value |
|---|---|
kind |
:exception for capture_exception/rescued errors, :message for capture_message
|
exception |
the exception (nil for :message) |
message |
the message text (nil for :exception) |
source |
where it came from ("Sidekiq: MyWorker", "manual", ...) |
context |
the context hash passed to the notifier |
fingerprint |
grouping key of the exception — stable across repeats, use it to deduplicate whatever the button creates (nil for :message) |
Return an array of rows ([[button, button], [button]]); a flat array becomes a single row; nil or [] means no keyboard. Exceptions raised inside the callable are logged and the notification is still delivered — without the keyboard.
Updating buttons later
notify, capture_exception and capture_message return the ids of the delivered message:
result = TgErrorNotifier.capture_exception(error)
# => { sent: true, status: :sent, code: 200, message_id: 812, chat_id: -1001234567890, thread_id: 15 }Keep them if the notification should reflect what happened next — e.g. replace "To the board" with a link to the task once it exists:
TgErrorNotifier.edit_buttons(
chat_id: stored.chat_id,
message_id: stored.message_id,
buttons: [[{ text: "Task #42 · In progress", url: "https://example.com/tasks/42" }]]
)buttons takes rows in the same shape as config.buttons; nil or [] removes the keyboard. Only the keyboard changes, the text stays as it was. The result is a hash with status:
| status | meaning |
|---|---|
:edited |
keyboard replaced |
:not_modified |
keyboard is already the same |
:not_found |
the message was deleted — forget its ids |
:rate_limited |
Telegram asked to wait retry_after seconds |
:skipped |
notifier is disabled in this environment |
:failed |
any other API error, with code and body
|
Unlike notifications, which never raise, edit_buttons lets network errors through: it is called by your code (usually a background job), which knows whether to retry. Telegram allows roughly 20 edits per minute in a group, so update the latest few messages of an error, not all of them.