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@BotNamestripped). The full argument string is stored inproperties[: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) —
datais stored verbatim. - Inline queries —
queryis stored verbatim. - Response time — attached to every action event as
_response_time_ms+_response_time_handler - First-seen users —
$identifywith username, first_name, language_code, is_premium (bounded 50k LRU) - Errors — any raised
StandardErrorbecomes a$errorevent with fingerprint - Blocked users — 403 from Telegram becomes a
user_blockedevent withlast_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.
Links
License
MIT.