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"
end

Environment variables

  • TELEGRAM_BOT_TOKEN
  • TELEGRAM_ERRORS_CHAT_ID
  • TELEGRAM_ERRORS_APP_NAME (optional)

Telegram setup (bot, private channel/group, chat_id)

1. Create a bot and get token

  1. Open Telegram and start chat with @BotFather.
  2. Send /newbot and follow prompts (bot name + username ending with bot).
  3. Copy the HTTP API token (looks like 123456:ABC...) and save it as TELEGRAM_BOT_TOKEN.

2. Create a private channel or private group

Private channel

  1. Telegram -> New Channel -> set as Private.
  2. Open channel settings -> Administrators -> add your bot as admin.
  3. Grant at least permission to post messages.

Private group (or supergroup)

  1. Telegram -> New Group -> add at least one member.
  2. Add your bot to the group.
  3. Promote bot to admin if needed (recommended for reliability).
  4. 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

  1. Send at least one message in target chat after adding your bot.
  2. Open in browser: https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/getUpdates
  3. Find chat object and copy chat.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_ID and 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)
end

Errors 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
end

Requirements: 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
end

Manual 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
end

capture_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: #{.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) }
end

Without 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.

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
end

Arguments:

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.