tgvizor

Analytics SDK for Telegram bots. Track events, errors, performance, user journeys, and blocked users with one line of middleware. Zero runtime dependencies.

gem install tgvizor

Ruby >= 3.1.

telegram-bot-ruby

require 'telegram/bot'
require 'tgvizor'
require 'tgvizor/middleware/telegram_bot_ruby'

vizor   = TgVizor::Client.new(api_key: ENV['TGVIZOR_API_KEY'])
tracker = TgVizor::Middleware::TelegramBotRuby.new(vizor)

Telegram::Bot::Client.run(ENV['BOT_TOKEN']) do |bot|
  bot.listen do |update|
    tracker.track(update) do
      # your existing handler — unchanged
    end
  end
end

What gets tracked automatically

  • Commands (/start, /help, etc., with @BotName stripped). The full argument string is stored in properties[:args].
  • Messages by type (text, photo, voice, sticker, video, document, audio, animation, video_note, location, contact, poll, dice). The plain text content of text messages is NOT captured by default — see "Capturing message text" below.
  • Callback queries (button presses) — data is stored verbatim.
  • Inline queriesquery is stored verbatim.
  • Response time — attached to every action event as _response_time_ms + _response_time_handler
  • First-seen users$identify with username, first_name, language_code, is_premium (bounded 50k LRU)
  • Errors — any raised StandardError becomes a $error event with fingerprint
  • Blocked users — 403 from Telegram becomes a user_blocked event with last_action

The middleware re-raises every exception so your existing error handling stays intact.

Capturing message text

By default, the middleware stores only the type of plain (non-command) messages, not the content. This is the privacy-safe default — plain messages can contain anything (emails, card numbers, personal info), and storing user-generated content has GDPR implications you'll want to opt into explicitly.

For bots where the message content IS the signal — AI chatbots, translators, URL extractors, search bots — enable it explicitly:

tracker = TgVizor::Middleware::TelegramBotRuby.new(
  vizor,
  capture_message_text:    true,   # default false
  max_message_text_length: 500,    # default 500; longer is truncated with "…"
)

Captured text lands in properties[:text] on the message event and is visible in the Event Explorer + User Journey pages. Commands always capture args regardless of this setting — those are structured arguments you designed.

Custom events

vizor.track('purchase', user_id: update.from.id, properties: { amount: 9.99 })

vizor.identify(update.from.id, username: update.from.username, language_code: update.from.language_code)

begin
  handle_checkout
rescue => e
  vizor.capture_error(e, user_id: update.from.id, command: '/checkout', extra: { cart: 3 })
  raise
end

Graceful shutdown

at_exit do
  vizor.shutdown!
end

(The SDK registers this automatically; listed here for reference.)

Disk fallback

If the ingestion API is unreachable after max_retries attempts, events persist to .tgvizor/events.jsonl (capped at 10 MB) and drain automatically when connectivity returns. Disable with persist_queue: false.

Configuration

TgVizor::Client.new(
  api_key: 'pk_live_xxx',               # required

  endpoint: 'https://ingest.tgvizor.com',
  flush_interval: 5,                     # seconds
  max_queue_size: 10_000,
  max_retries: 5,
  persist_queue: true,
  batch_size: 500,
)

HTTP fallback

No Ruby? Any language can POST to https://ingest.tgvizor.com/v1/events directly. See api-reference.md.

License

MIT.