Skip to main content

Events

Events are the foundation of analytics in Metriox. Every user action in your Telegram bot can be recorded as an event.

What is an event?

An event is any action or interaction between a user and your bot:

  • A button press
  • A command being sent
  • A screen view
  • A completed purchase
  • Any custom action

Event structure

Events in Metriox are platform-agnostic and consist of:

Metriox events table with the Timestamp (UTC), Event, Text, User, Source, Origin, Type and Details columns, showing callback_query and message rows

Core fields

FieldTypeDescription
event_idUUIDUnique identifier of the event
event_nameStringEvent name (for example, message)
platform_user_idStringUser ID in Telegram
session_idUUIDUser session ID
tenant_idUUIDID of your bot in Metriox
received_atDateTimeTime the server received the event
created_atDateTimeTime the event was created

Additional fields

  • body (String, nullable) — the text content of the event
  • props (JSON) — arbitrary event properties in JSON format

One format, whatever the source

Data can reach Metriox in several ways — through a bot token (polling Telegram over MTProto), through a server-side SDK on top of the Bot API, through the WebApp SDK in the browser, or directly over the HTTP API. Whatever the source, Metriox normalizes every event into a single canonical structure on the server: the same event names, the same $ property sections, and the same field values.

That means data of any origin is equally visible and usable in the dashboard, in filters, and in segments — you never need to know how an event was captured in order to report on it.

How it works

Normalization happens once, at ingest. The fewer transformations, the more predictable the data, so the SDKs and the worker send fields as close to their final shape as possible and the server only finishes standardizing them. This does not break historical data: older events are read back in the same canonical shape.

Canonical event names (Telegram)

Telegram events get a canonical name based on the meaning of the action, not on how it was delivered. The key principle: one action, one name — the details are expressed as properties.

For example, any message — incoming or outgoing, plain text or with an attachment, new or edited — is a single message event. You tell them apart by their properties:

PropertyWhat it tells you
$tg.directionoutbound — a bot reply, inbound — a user message
$tg.is_editedthe message was edited
$tg.message_typecontent type (text, photo, …)
$tg.chat_typechat type (private, group, supergroup, channel)
$tg.entitiestext formatting (bold, links, code)

The main canonical names:

Event nameWhen it happens
messageany message (see the properties above)
callback_queryan inline button was pressed
inline_queryan inline query to the bot
chosen_inline_resultan inline result was chosen
poll / poll_votea poll / a vote in a poll
reactiona reaction to a message
pre_checkout_query, shipping_query, successful_paymentpayment stages
my_chat_member, chat_member, chat_join_requesta change in chat membership (including the bot being blocked)
message_deletedmessages were deleted
business_messagea message sent through Telegram Business
Direction in a conversation

Metriox uses $tg.direction to render a conversation as two sides: user messages on the left, bot replies on the right.

The field is filled in on every source, but you still have to be able to see the bot's replies. By design, the Bot API never delivers a bot's own outgoing messages back to it, so on the Bot API the bot reports them itself: wrap your client with WithMetrioxCapture(...) in the C# SDK and this happens automatically. A bot-token connection (MTProto) sees outgoing messages on its own, but it does not receive direct messages — only the SDK brings those in. See SDK for details.

Event identity and deduplication

event_id is computed deterministically from Telegram's "natural" coordinates — for example, from the chat ID and the message ID. That has two practical consequences:

  • redelivery does not double your data. If Telegram or your bot sends the same update twice (a restart, a webhook retry, a reconnect), it stays a single event;
  • running both connections does not double your bill. If the same bot is connected both by token and through the SDK, both paths compute the same event_id for one action.
What actually happens with both connections active

This is easy to get wrong, so precisely:

Result
event_id of the two copiesidentical
Rows storedtwo — they do not collapse into one
Charged against your event quotaonce

The rows stay separate on purpose: they carry different $source.channel values, and their session_id and platform_user_id may differ too, so a report pinned to one source still finds its data. The second copy is marked with $meta.unbilled_duplicate and consumes no quota.

Charge deduplication does not apply to every event type — only to those where both connections report the same natural Telegram coordinates: ordinary messages, edits (when Telegram supplied an edit time), button presses, inline queries, payment queries, and business-account messages.

Still counted separately: reactions, poll votes and chosen inline results. For every other type the question does not arise — only one connection can physically see them, so no duplicate exists.

Why not update_id

update_id from the Bot API may look like the obvious choice. It is not: update_id exists only in the Bot API getUpdates/webhook stream, and MTProto has no such thing at all — there, updates are numbered by session cursors (pts/qts) that differ for every client and are not a property of the event itself. That is why the identifier is built from coordinates that both connection methods can see.

Your custom events use any name you choose (purchase_completed, screen_view, …) and are marked with $event.origin = custom.

Event properties

Properties are extra information about an event that lets you analyze user behavior in more detail.

There are two kinds of properties: system properties (captured automatically by the platform, prefixed with $) and custom properties (your own arbitrary fields). See Properties for details.

Property examples

{
"button_name": "buy_subscription",
"plan": "premium",
"price": 299,
"currency": "RUB",
"screen_name": "pricing"
}

Property types

Metriox supports the following terminal data types:

  • String — text values
  • Number — numeric values (integer, float)
  • DateTime — date and time
  • Boolean — true/false
Tip

Use clear names for custom events and properties. For example, prefer subscription_button_click over btn_1_click.

Event examples

A simple custom event

{
"event_name": "bot_started",
"platform_user_id": "123456789",
"session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

A custom event with properties

{
"event_name": "purchase_completed",
"platform_user_id": "123456789",
"props": {
"product_id": "premium_month",
"amount": 299,
"currency": "RUB",
"payment_method": "telegram_stars"
}
}

A Telegram message captured automatically

This is what the /start premium command from a user looks like after normalization. The event name is message, and the Telegram details live in the read-only system sections $event and $tg:

{
"event_name": "message",
"platform_user_id": "123456789",
"body": "/start premium",
"props": {
"$event": { "origin": "telegram", "type": "message" },
"$tg": {
"is_outgoing": false,
"chat_type": "private",
"command": "/start",
"command_params": "premium"
}
}
}

Sending events

You can send events in several ways:

  1. SDK — use the official SDKs for JavaScript/TypeScript or C#
  2. API — send events through the REST API
  3. Automatically — Telegram events are captured automatically once you connect a bot token

Best practices

  • Use snake_case for custom event names: button_click, purchase_completed
  • Add contextual properties: screen_name, source, category
  • Group similar events with properties rather than with dozens of unique names
  • Names that are too generic: click, action, event
  • Sending personal data in plain form
  • Too many unique events (properties are the better tool)

What's next?