Class: TgVizor::Middleware::TelegramBotRuby

Inherits:
Object
  • Object
show all
Defined in:
lib/tgvizor/middleware/telegram_bot_ruby.rb

Overview

Middleware for the telegram-bot-ruby gem (github.com/atipugin/telegram-bot-ruby).

Wraps your update-handling block with auto-tracking of:

- the update type (command / message / callback_query / inline_query)
- response time (handler duration in ms)
- $identify on first-seen-per-process for each user
- $error on any StandardError raised by the handler
- user_blocked when the handler triggers a 403 from Telegram

Re-raises every exception so the host bot’s existing error handling stays intact. TgVizor is an observer, not a control-flow modifier.

Examples:

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(TOKEN) do |bot|
  bot.listen do |update|
    tracker.track(update) do
      # your existing handler, unchanged
    end
  end
end

Constant Summary collapse

MESSAGE_TYPES =
%i[
  text photo voice sticker video document audio animation
  video_note location contact poll dice
].freeze
SEEN_USERS_CAP =

Bound on the in-process “have we identified this user yet?” set. Uncapped, a long-running bot serving millions of users would grow this forever (~80 MB at 1M ids). When the cap is hit we drop a random half —those users will simply re-fire identify on their next interaction, which is harmless.

50_000
DEFAULT_MAX_MESSAGE_TEXT_LENGTH =

Short enough that a spammer can’t inflate the events table with a 10k-char wall of text; long enough for most natural-language prompts.

500

Instance Method Summary collapse

Constructor Details

#initialize(client, capture_message_text: false, max_message_text_length: DEFAULT_MAX_MESSAGE_TEXT_LENGTH) ⇒ TelegramBotRuby

‘capture_message_text` is opt-in because plain messages can contain PII (emails, card numbers, personal info). Storing user-generated content carries GDPR obligations most bot owners will want to opt into explicitly. Safe to enable for bots whose plain-text traffic is the signal itself: AI chatbots, translators, URL extractors, search bots. Commands always capture `args` regardless of this setting.



57
58
59
60
61
62
63
64
65
66
67
# File 'lib/tgvizor/middleware/telegram_bot_ruby.rb', line 57

def initialize(
  client,
  capture_message_text: false,
  max_message_text_length: DEFAULT_MAX_MESSAGE_TEXT_LENGTH
)
  @client                  = client
  @seen_users              = Set.new
  @seen_mutex              = Mutex.new
  @capture_message_text    = capture_message_text
  @max_message_text_length = max_message_text_length
end

Instance Method Details

#track(update) { ... } ⇒ Object

Wrap a single update through the analytics layer.

Parameters:

  • update (Telegram::Bot::Types::Base)

    message, callback query, inline query, …

Yields:

  • runs your handler with the update

Returns:

  • (Object)

    whatever the block returns



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/tgvizor/middleware/telegram_bot_ruby.rb', line 74

def track(update, &block)
  classification = classify(update)
  user_id        = extract_user_id(update)
  started_at     = monotonic_now

  identify_once(update, user_id)

  block.call
rescue Telegram::Bot::Exceptions::ResponseError => e
  if e.error_code.to_i == 403
    # Prefer the specific command text (e.g. "/promo_weekly") over the generic
    # event class — bot owners want to know which action drove the block.
    last_action = classification[:command] || classification[:event] || "unknown"
    @client.track(
      "user_blocked",
      user_id:    user_id,
      properties: { last_action: last_action },
    )
  else
    @client.capture_error(e, user_id: user_id, command: classification[:command])
  end
  raise
rescue StandardError => e
  @client.capture_error(e, user_id: user_id, command: classification[:command])
  raise
ensure
  if classification && classification[:event]
    elapsed_ms = started_at ? ((monotonic_now - started_at) * 1000).round : nil
    emit_classified_event(classification, user_id, elapsed_ms)
  end
end