Источник запуска Telegram-бота: start-метки и повторные обращения

6 мин чтения

Как передать источник через start, проверить метку и сохранить первый известный источник при повторном запуске. Локальный Python-тест и ограничения атрибуции.

Как учитывать источник запуска Telegram-бота через start

Для каждого размещения заведите короткую метку и ссылку вида https://t.me/YOUR_BOT_USERNAME?start=channel_a. Когда бот получит /start channel_a, сверьте метку со справочником и сохраните событие. Первый известный источник храните отдельно от последнего: новый запуск не должен его перезаписывать.

Это атрибуция наблюдаемых запусков, а не кликов. Метка показывает, с каким параметром человек запустил бота, но не доказывает, где он увидел ссылку. Ниже описана схема для собственного бота в личном чате и приведён локальный Python-тест. Это не инструкция по настройке функции 24GO.

Что Telegram передаёт боту

Документация Telegram о deep linking описывает передачу параметра через start. Разрешены латинские буквы A-Z, a-z, цифры 0-9, _ и -; длина параметра не должна превышать 64 символа. Короткую метку вроде channel_a кодировать не нужно.

Замените YOUR_BOT_USERNAME на username своего бота. Это пример, а не ссылка на демонстрационный сервис:

https://t.me/YOUR_BOT_USERNAME?start=channel_a
https://t.me/YOUR_BOT_USERNAME?start=webinar_b

Спецификация bot links предусматривает кнопку Start, если передан параметр, в том числе когда пользователь уже начинал диалог. Убедитесь, что сервер получает команду после действия пользователя. Само открытие ссылки ещё не означает, что серверу поступило событие. Интерфейс каждого используемого вами клиента проверяйте отдельно.

Не путайте start для личного чата со startgroup, который используют при добавлении бота в группу. Произвольные поля utm_source в ссылке не заменяют обработку start. Если боту нужен идентификатор кампании, передайте распознаваемый payload: метку источника.

Сначала определите, что означает источник

Заведите справочник: channel_a обозначает одно разрешённое размещение, webinar_b - другое. На сервере храните описание каждой метки, период её использования и ответственного. Не включайте в ссылку номера телефонов, email-адреса, токены доступа или другие чувствительные данные. Base64url меняет представление данных, но не шифрует их.

Разделите журнал событий и запись атрибуции пользователя:

ПолеЧто сохранятьПравило
observed_start_atВремя, когда сервер получил командуНе называть временем клика
source_statusknown, missing или invalid_or_unknownНе придумывать источник, если метки нет
source_codeПроверенный код либо пустое значениеНе доверять произвольному тексту
first_known_sourceПервую принятую меткуСохранять при следующих запусках
last_known_sourceПоследнюю принятую меткуОбновлять только при известной метке
«Первый известный» - намеренно узкое определение. Человек мог сначала прийти без метки, а позже открыть ссылку на вебинар. В таком случае вебинар - первый источник, который удалось определить, но не обязательно первое взаимодействие с компанией. Если важно сохранить первый наблюдаемый запуск независимо от метки, записывайте его отдельно, в том числе со статусом missing.

Используйте стабильный внутренний ключ пользователя и ограничьте доступ к нему. В продакшене обновляйте запись атомарно, чтобы параллельные события не могли оба считаться первыми. Журнал доставки и дедупликация событий решают другие задачи; ни то ни другое не превращает повторный /start в нового лида.

Локальный тест: первая метка сохраняется

Пример проверяет только вашу политику обработки аргумента, уже извлечённого из /start. Он не запускает бота, не принимает вебхуки и не подключается к CRM. Нужен Python 3; дополнительные зависимости не требуются. Сохраните код в attribution_test.py и выполните python attribution_test.py:

import json
import re

KNOWN = {"channel_a", "webinar_b"}
PATTERN = re.compile(r"[A-Za-z0-9_-]{1,64}")

def valid_payload(value):
return isinstance(value, str) and PATTERN.fullmatch(value) is not None

def observe(state, payload):
if payload is None or payload == "":
status = "missing"
elif not valid_payload(payload) or payload not in KNOWN:
status = "invalid_or_unknown"
else:
status = "known"
if state["first_known"] is None:
state["first_known"] = payload
state["last_known"] = payload
return status

assert valid_payload("a" * 64)
assert not valid_payload("a" * 65)
assert not valid_payload("has space")
state = {"first_known": None, "last_known": None}
inputs = [None, "channel_a", "webinar_b", "made_up", "", "webinar_b"]
statuses = [observe(state, value) for value in inputs]
assert statuses == ["missing", "known", "known", "invalid_or_unknown", "missing", "known"]
assert state == {"first_known": "channel_a", "last_known": "webinar_b"}
print(json.dumps({"statuses": statuses, "state": state}, sort_keys=True))

Ожидаемый вывод:

{"state": {"first_known": "channel_a", "last_known": "webinar_b"}, "statuses": ["missing", "known", "known", "invalid_or_unknown", "missing", "known"]}

Проверка формата и поиск в справочнике - разные операции. Строка из 64 букв проходит проверку формата, но не считается известным источником, если её нет в KNOWN. Неизвестная или отсутствующая метка не стирает принятые ранее значения. Повторный webinar_b оставляет оба сохранённых значения без изменений.

Это синтетический тест одного пользователя с данными в памяти. Он не проверяет доставку сообщений Telegram, устойчивость хранения к сбоям или обработку параллельных запросов. Исключение AssertionError означает, что результат не соответствует описанной политике: разберитесь в причине, а не удаляйте проверку. Не запускайте тест с python -O: этот режим отключает assert.

Как проверить своего бота до распространения ссылок

  1. Добавьте две метки в свой справочник и настройте обработчик /start: он должен извлечь аргумент команды и передать его валидатору. Приведённый код не заменяет обработчик.
  2. Откройте первую ссылку в тестовом аккаунте и нажмите Start, если клиент предлагает такую кнопку. Убедитесь, что сервер получил ожидаемую команду и записал channel_a в оба поля.
  3. В том же аккаунте откройте вторую ссылку и запустите бота. Ожидаемые значения: first_known_source=channel_a и last_known_source=webinar_b. Не создавайте вторую карточку лида только из-за повторного запуска.
  4. Отправьте /start без аргумента, затем /start made_up. В журнале должны появиться соответствующие статусы, а известные источники должны сохраниться.
  5. Повторите тест в тех клиентах Telegram, которые используете. Запишите клиент, версию, время, полученную команду и результат в базе. Не включайте токен бота в отчёт.
Если команда не поступила, выясните, связана ли проблема с открытием ссылки, доставкой обновления или фильтрацией в обработчике. Если команда пришла, но метка потерялась, проверьте извлечение аргумента, справочник и запись в базу. Ни один из этих случаев сам по себе не доказывает отсутствие спроса на кампанию.

Что эти записи показывают, а что нет

Публичную ссылку можно переслать, а её код - ввести вручную. Whitelist подтверждает, что код вам известен, но не доказывает, что пользователь пришёл с предназначенного для него размещения. Подписанный параметр тоже не мешает скопировать ссылку целиком. Не используйте обычную campaign-метку, чтобы предоставлять доступ, выплачивать вознаграждение или выдавать другие привилегии.

Можно считать принятые команды и отдельно - уникальных внутренних пользователей с известной меткой за явно заданный период. Повторные запуски одного пользователя не равны новым обращениям. Для квалификации нужен отдельный критерий, а факт продажи следует подтверждать отдельно в системе продаж.

Сам бот не даёт знаменатель для CTR: он не показывает все открытия ссылок, после которых пользователь не запустил диалог. Даже собственный счётчик перенаправлений может учитывать предварительные просмотры и роботов. Не делите число запусков на несопоставимое число просмотров и не называйте результат конверсией всей аудитории.

Наконец, получение /start само по себе не означает согласия на дальнейший маркетинг. Объясняйте цель сбора данных, ограничивайте доступ и срок хранения. Условия отправки сообщений и применимые требования оценивайте отдельно. Результат этой схемы - проверяемый источник наблюдаемого запуска, а не разрешение писать человеку и не доказательство ROI.