Elido
10 мин чтенияИнтеграции

Вебхуки против поллинга для отслеживания кликов - выбираем правильный паттерн

Практический разбор того, когда использовать вебхуки, а когда опрашивать API аналитики для получения данных о кликах: скрытые расходы каждого подхода, конкретные примеры кода на TypeScript и Python, а также гибридный паттерн, который покрывает большинство случаев использования в продакшене.

Sasha Ehrlich
Compliance · EU residency
Split diagram comparing webhooks and polling for click tracking: left panel shows server pushing click.recorded events to receiver endpoint; right panel shows cron hitting /analytics/summary every N minutes

Две команды, создающие интеграцию на базе одного и того же API сокращателя ссылок, часто приходят к совершенно разным архитектурам. Одна команда настраивает эндпоинт для вебхуков и реагирует на каждый клик в режиме реального времени. Другая пишет задачу cron, которая опрашивает API аналитики каждые пять минут. Оба варианта допустимы. Выбор между ними имеет реальные последствия для задержки (latency), эксплуатационных расходов и того, насколько ваша система будет деградировать при возникновении проблем.

В этом посте представлены основные компромиссы.

Два паттерна#

Поллинг#

Поллинг (polling) означает, что ваш код запрашивает у API последние данные о кликах по расписанию. Задача cron просыпается, вызывает /v1/analytics/workspaces/{id}/clicks/recent или /v1/analytics/workspaces/{id}/summary, обрабатывает результаты и засыпает до следующего интервала.

Поток данных основан на «вытягивании» (pull-based): ваша инфраструктура инициирует каждое взаимодействие. Сервер API ничего не знает о ваших внутренних системах - он просто отвечает на отправленные вами запросы.

Вебхуки#

Вебхуки (webhooks) означают, что сервер Elido отправляет событие click.recorded на ваш HTTPS-эндпоинт вскоре после обработки клика. Ваш приемник обрабатывает его, возвращает 2xx, и доставка фиксируется как успешная.

Поток данных основан на «выталкивании» (push-based): платформа инициирует контакт. Ваш эндпоинт должен быть доступен из интернета, иметь TLS и надежно отвечать.

Сравнение рядом: при поллинге ваша задача cron отправляет GET в Elido API и забирает строки каждые пять минут; при вебхуках сервер Elido отправляет POST с click.recorded на ваш HTTPS-эндпоинт, который возвращает 2xx.

Когда поллинг - правильный выбор#

Поллинг подходит для определенного набора условий. Если к вашей ситуации применимо большинство из нижеперечисленного, начните с поллинга и переходите к вебхукам только тогда, когда возникнет конкретная проблема.

Вы контролируете обе стороны интеграции. Когда потребителем является дашборд или инструмент отчетности, которым вы владеете и управляете, поллинг обеспечивает предсказуемое, ограниченное поведение. Вы сами определяете интервал, временное окно и способ обработки частичных результатов.

Ваш вариант использования - ретроспективный. Еженедельные отчеты по кампаниям, ежемесячные задачи агрегации и конвейеры сверки данных не выигрывают от субминутной задержки. Задачу cron, запускаемую каждый час для /summary или /breakdown/country, архитектурно проще реализовать и отлаживать, чем приемник вебхуков с сохранением состояния и обработкой повторных попыток.

У вас нет публичного эндпоинта. Вебхуки требуют наличия URL, доступного для инфраструктуры Elido. Если ваша интеграция работает внутри частной сети, в функции Lambda без стабильного URL или на локальной машине разработчика, настройка входящего эндпоинта HTTPS может обойтись дороже с точки зрения сложности эксплуатации, чем выгода от скорости получения данных.

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

Когда вебхуки - правильный выбор#

Вебхуки имеют смысл, когда низкая задержка является требованием к продукту, а не просто приятным дополнением.

Вы создаете живой счетчик или real-time интерфейс. Если ваш продукт показывает пользователям количество кликов, которое обновляется в течение нескольких секунд после перехода по ссылке, поллинг с любым разумным интервалом будет казаться заметно устаревшим. Обработчик вебхуков, который увеличивает счетчик в Redis при событиях click.recorded и выводит его на фронтенд через соединение WebSocket или SSE - это архитектура, позволяющая добиться этого, не перегружая API аналитики.

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

Вы запускаете рабочие процессы, управляемые событиями (event-driven). Рабочие процессы, инициируемые событиями кликов - отправка последующего письма при переходе по ссылке, обновление сегмента подписчика, уменьшение количества товара на складе - являются естественными потребителями вебхуков. Событие click.recorded несет в себе достаточно данных, чтобы действовать немедленно, без дополнительного запроса.

У вас есть стабильный, публично доступный HTTPS-эндпоинт. Это обязательное условие, от которого зависит все остальное. Если у вас уже есть рабочая инфраструктура, принимающая входящие вебхуки от других провайдеров (Stripe, GitHub, Twilio), добавление Elido в тот же приемник не составит труда.

Скрытые расходы вебхуков#

Вебхуки звучат просто: сервер отправляет POST, вы его обрабатываете. Реальная поверхность реализации намного шире.

Многоуровневая диаграмма четырех шлюзов, через которые проходит каждая доставка click.recorded: проверка подписи HMAC, 300-секундное окно защиты от повторов, дедупликация по ID доставки и необходимость вернуть 2xx до истечения тайм-аута.

Проверка подписи#

Elido подписывает каждую доставку вебхука с помощью HMAC-SHA256. Формат подписи: v1=HMAC-SHA256(secret, "${unix_timestamp}.${body}"), передается в заголовке X-Webhook-Signature. Временная метка отправляется отдельно в X-Webhook-Timestamp. Оба значения генерируются в services/webhook-dispatcher/internal/signing/hmac.go.

Вы должны проверить эту подпись перед обработкой полезной нагрузки. Приемник, который пропускает проверку, будет обрабатывать любой POST-запрос, достигший эндпоинта, включая поддельные запросы от любого, кто узнает URL вашего вебхука.

Вот минимальный обработчик Express на TypeScript, который проверяет подпись перед выполнением каких-либо действий:

import express, { Request, Response } from "express";
import crypto from "crypto";

const app = express();

// Используйте raw body middleware - парсеры JSON потребляют поток до того, как вы сможете создать хэш
app.use("/webhook", express.raw({ type: "application/json" }));

function verifySignature(
  secret: string,
  signature: string,
  timestamp: string,
  rawBody: Buffer,
): boolean {
  const message = `${timestamp}.${rawBody.toString("utf8")}`;
  const expected =
    "v1=" + crypto.createHmac("sha256", secret).update(message).digest("hex");
  // Используйте timingSafeEqual для предотвращения перебора на основе времени
  return crypto.timingSafeEqual(
    Buffer.from(signature, "utf8"),
    Buffer.from(expected, "utf8"),
  );
}

app.post("/webhook", (req: Request, res: Response) => {
  const signature = req.headers["x-webhook-signature"] as string;
  const timestamp = req.headers["x-webhook-timestamp"] as string;

  if (!signature || !timestamp) {
    return res.status(400).json({ error: "missing signature headers" });
  }

  // Отклоняем полезную нагрузку старше 5 минут
  const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
  if (age > 300) {
    return res.status(400).json({ error: "payload too old" });
  }

  if (
    !verifySignature(
      process.env.WEBHOOK_SECRET!,
      signature,
      timestamp,
      req.body as Buffer,
    )
  ) {
    return res.status(401).json({ error: "invalid signature" });
  }

  const event = JSON.parse((req.body as Buffer).toString("utf8"));

  if (event.type === "click.recorded") {
    // Обработка события клика
    console.log("click recorded:", event.data);
  }

  // Всегда быстро возвращайте 2xx - тяжелую обработку делайте асинхронно
  return res.status(200).json({ received: true });
});

Окно повтора#

Проверка временной метки в приведенном выше примере обеспечивает то, что документация Elido называет «окном повтора» (replay window). Без нее злоумышленник, перехвативший одну валидную подписанную полезную нагрузку, может воспроизводить ее бесконечно - подпись остается действительной навсегда, так как она вычисляется на основе фиксированной временной метки. С проверкой полезная нагрузка старше пяти минут отклоняется независимо от того, валидна ли подпись.

Установите допуск на значение, которое может выдержать ваша инфраструктура. Пять минут - это стандартное значение по умолчанию, аналогичное тому, что использует Stripe. Если ваш приемник иногда отключается на несколько минут во время развертывания, это окно дает ему время вернуться в строй и все равно обработать запросы, находящиеся в пути.

Повторные попытки и идемпотентность#

webhook-dispatcher в Elido повторяет неудачные доставки по графику: первая попытка через 1 минуту, вторая через 5 минут, третья через 15 минут. Максимальное количество попыток на одну доставку по умолчанию равно 3, как определено в схеме webhook_deliveries. После 3 неудачных попыток доставка помечается как окончательно неудавшаяся и отображается в дашборде уведомлений.

Это означает, что ваш приемник может получить одно и то же событие более одного раза. Любая обработка, имеющая побочные эффекты - запись в базу данных, отправка письма, обновление счетчика - должна быть идемпотентной. Заголовок X-Webhook-Delivery содержит стабильный ID доставки, который вы можете использовать в качестве ключа идемпотентности.

// Перед обработкой проверьте, была ли эта доставка уже обработана
const deliveryId = req.headers["x-webhook-delivery"] as string;
const alreadyProcessed = await redis.get(`webhook:delivery:${deliveryId}`);
if (alreadyProcessed) {
  return res.status(200).json({ received: true, duplicate: true });
}
// Помечаем как обработанное с TTL, покрывающим окно повторных попыток
await redis.set(`webhook:delivery:${deliveryId}`, "1", "EX", 3600);

Ваш эндпоинт должен быть высокодоступным#

Окно повторных попыток конечно. Если ваш приемник не работает более примерно 21 минуты (1 + 5 + 15), попытки доставки будут исчерпаны, и они завершатся окончательной неудачей. Для событий, где гарантированная доставка важна - обогащение CRM, биллинг - инфраструктура вашего приемника нуждается в надлежащей доступности, а не в «домашнем» сервере, который периодически перезагружается.

Это наиболее недооцениваемая стоимость вебхуков для команд, впервые сталкивающихся с входящими HTTP-запросами. Поллинг деградирует изящно: если задача поллинга не удалась, она просто запустится снова в следующий интервал и нагонит упущенное. Приемник вебхуков, который недоступен, теряет события навсегда, если у вас нет стратегии сверки.

Скрытые расходы поллинга#

Со стороны поллинг выглядит простым. Реальные затраты накапливаются в процессе эксплуатации.

Задержка (lag) - определяющее ограничение. Задача cron, работающая каждые пять минут, означает, что данные о кликах устарели на пять минут. Для большинства ретроспективных сценариев это приемлемо; для всего, что видит пользователь - нет. Сокращение интервала помогает, но не устраняет задержку, а очень короткие интервалы (менее минуты) начинают походить на «бомбардировку» API запросами, а не на поллинг.

Бесполезные запросы. В большинстве интервалов поллинга возвращаются те же данные, что и в предыдущем запросе. Если вы опрашиваете ссылку с низким трафиком каждую минуту, а клики происходят примерно раз в час, 59 из каждых 60 запросов не приносят ничего нового. Эти запросы все равно учитываются в лимитах API.

Лимиты скорости (rate limits). API Elido применяет лимиты скорости для каждого рабочего пространства (workspace), размер которых зависит от тарифного плана. Задача поллинга, которая часто выполняется для многих ссылок в большом рабочем пространстве, может достичь этих лимитов, особенно если другая автоматизация в том же пространстве также делает вызовы API. В этом случае API возвращает 429 Too Many Requests с заголовком X-RateLimit-Scope: workspace.

Пагинация и пропущенные события. Эндпоинт /clicks/recent использует пагинацию на основе курсора. Если вы выполняете поллинг в фиксированном временном окне - ?from=<last_poll>&to=<now> - и объем данных в этом окне превышает размер страницы, вы пропустите события, если не будете следовать за next_cursor по всем страницам. Реализация поллинга, которая не обрабатывает пагинацию, будет молча терять данные под нагрузкой.

Гибридный паттерн#

Для большинства рабочих сценариев лучшим ответом является не выбор «или/или».

Диаграмма гибридной архитектуры: Elido отправляет события кликов обработчику вебхуков как основной путь реального времени, а ночной поллинг /summary и /timeseries сверяет итоги в вашем хранилище и заполняет пробелы, оставшиеся от окна повторных попыток.

Используйте вебхуки как основной путь для реакции в реальном времени: обновления CRM, живые счетчики, рабочие процессы, управляемые событиями. Задержка низкая; эксплуатационные расходы управляемы, если у вас уже есть инфраструктура для входящих HTTPS-запросов.

Используйте поллинг для еженедельной или ежедневной сверки: загрузите полный временной ряд за предыдущую неделю, сравните итоги с тем, что зафиксировал ваш обработчик вебхуков, и выявите любые пробелы. Это позволяет обнаружить доставки, которые исчерпали окно повторных попыток во время сбоя, события, которые пришли не по порядку, и любые расхождения между вашим локальным состоянием и источником истины Elido.

API аналитики хорошо подходит для этой роли. Эндпоинт /summary возвращает агрегированные итоги за диапазон дат в одном запросе; эндпоинт /timeseries возвращает данные по дням. Задача сверки, которая запускается раз в ночь и сравнивает количество кликов в вашей CRM с данными API за то же окно, может выявить проблемы с целостностью данных до того, как они станут заметны клиентам.

Cron для поллинга на Python#

Для команд, которые хотят начать с поллинга и перейти к вебхукам позже, вот минимальная реализация с использованием библиотеки schedule, которая вызывает /clicks/recent с пятиминутным интервалом:

import schedule
import time
import requests
import os

API_BASE = "https://api.elido.app/v1/analytics"
WORKSPACE_ID = os.environ["ELIDO_WORKSPACE_ID"]
API_KEY = os.environ["ELIDO_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

# Отслеживаем курсор между интервалами поллинга, чтобы получать только новые клики
_cursor = None

def poll_recent_clicks():
    global _cursor
    params = {"limit": 100}
    if _cursor:
        params["cursor"] = _cursor

    while True:
        resp = requests.get(
            f"{API_BASE}/workspaces/{WORKSPACE_ID}/clicks/recent",
            headers=HEADERS,
            params=params,
            timeout=10,
        )
        resp.raise_for_status()
        body = resp.json()

        items = body.get("items", [])
        for click in items:
            process_click(click)

        next_cursor = body.get("next_cursor")
        if not next_cursor:
            # Сохраняем текущий курсор для следующего запуска
            if items:
                _cursor = None  # сброс: следующий поллинг начнется с текущего момента
            break
        params["cursor"] = next_cursor

def process_click(click: dict):
    # Замените на вашу реальную логику обработки
    print(f"click: link={click['link_id']} country={click.get('country_code')}")

schedule.every(5).minutes.do(poll_recent_clicks)

if __name__ == "__main__":
    poll_recent_clicks()  # запуск один раз при старте, чтобы догнать данные
    while True:
        schedule.run_pending()
        time.sleep(10)

В рабочей среде замените print на реальный приемник данных - запись в базу данных, вызов API CRM, публикацию в очереди сообщений - и добавьте обработку ошибок с экспоненциальной задержкой вокруг вызова requests.get.

Фильтрация ботов и что она значит для вашей интеграции#

Одна деталь, которая влияет на оба паттерна: сервис редиректов Elido фильтрует клики ботов перед отправкой событий кликов в конвейер обработки. Запросы от Googlebot, Bingbot, Slackbot, мониторов аптайма, curl, скриптовых библиотек и пустых User-Agent не создают событий click.recorded и не отображаются в результатах API аналитики.

Это важно, потому что ваш обработчик вебхуков или задача поллинга работают с количеством кликов от людей, а не с общим количеством HTTP-запросов. Если вы сопоставляете данные о кликах Elido с метриками на стороне сервера - логами вашего приложения или логами доступа CDN - ожидайте, что цифры Elido будут ниже. Это расхождение не является ошибкой; это работа фильтра ботов, удаляющего шум до того, как он попадет к вам.

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


На странице с ценами приведена информация о том, какие тарифные планы включают эндпоинты вебхуков и каковы ограничения на объем доставки.

Похожее в блоге#

Попробуйте Elido

Вставьте URL - получите короткую ссылку

Без регистрации. Ссылка живёт 30 дней. Зарегистрируйтесь, чтобы оставить её навсегда.

Бесплатно, без регистрации · 2 в день

Попробуйте Elido

URL-сокращатель с хостингом в ЕС: собственные домены, глубокая аналитика, открытый API. Бесплатный тариф - без банковской карты.

Теги
click tracking
webhooks
url shortener api
link analytics
api integration
event-driven
polling
real-time analytics

Читать дальше