11 min czytaniaIntegracje

Webhooks a polling przy śledzeniu kliknięć - wybierz właściwy wzorzec

Praktyczne omówienie tego, kiedy używać webhooków, a kiedy odpytywać API analityczne o dane kliknięć: ukryte koszty obu podejść, konkretne przykłady kodu w TypeScript i Pythonie oraz wzorzec hybrydowy sprawdzający się w większości wdrożeń produkcyjnych.

Sasha Ehrlich
Compliance · EU residency
Podzielony diagram porównujący webhooks i polling dla śledzenia kliknięć: lewy panel pokazuje serwer przesyłający zdarzenia do punktu końcowego odbiorcy, z planowanymi zdarzeniami click.created dla poszczególnych kliknięć; prawy panel pokazuje cron uderzający do /analytics/summary co N minut

Dwa zespoły budujące integracje na tym samym API skracacza URL często trafiają na zupełnie różne architektury. Jeden chce mieć endpoint webhookowy reagujący na każde kliknięcie w czasie rzeczywistym. Drugi pisze cron job odpytujący API analityczne co pięć minut. Oba podejścia są słuszne, ale tylko jedno działa obecnie dla kliknięć w Elido: webhooki dla poszczególnych kliknięć są planowane, a nie wdrożone, więc bieżącą ścieżką dla danych o kliknięciach jest polling. Wybór między nimi ma realne konsekwencje dla opóźnień, kosztów operacyjnych i tego, jak Twój system degraduje się przy awariach.

Ten artykuł przedstawia rzeczywiste kompromisy.

Dwa wzorce

Polling

Polling oznacza, że Twój kod cyklicznie pyta API o ostatnie dane kliknięć. Cron job się budzi, wywołuje /v1/workspaces/{workspace_id}/analytics/clicks/recent lub /v1/workspaces/{workspace_id}/analytics/summary, przetwarza wyniki, a następnie zasypia do następnego interwału.

Przepływ danych jest oparty na zasadzie pull: Twoja infrastruktura inicjuje każdą interakcję. Serwer API nie ma wiedzy o Twoich wewnętrznych systemach - po prostu odpowiada na wysyłane zapytania.

Webhooks

Webhooks oznaczają, że serwer Elido wysyła zdarzenie na Twój endpoint HTTPS. Twój receiver obsługuje je, zwraca 2xx i dostarczenie jest zapisywane jako udane. Obecnie te zdarzenia obejmują linki i domeny (link.created, link.updated, link.expired, domain.verified i kilka innych). Zdarzenie click.created dla poszczególnych kliknięć jest planowane i nie jest jeszcze emitowane, więc na razie kliknięcie nie uruchamia żadnego webhooka.

Przepływ danych jest oparty na zasadzie push: platforma inicjuje kontakt. Twój endpoint musi być dostępny z internetu, musi posiadać TLS i musi odpowiadać niezawodnie.

Porównanie obok siebie: polling - Twój cron job wysyła GET do API Elido i pobiera wiersze co pięć minut; webhooks - serwer Elido wysyła POST na Twój endpoint HTTPS, który zwraca 2xx, a zdarzenie click.created dla poszczególnych kliknięć jest nadal planowane.

Kiedy polling jest właściwym wyborem

Polling sprawdza się w określonym zbiorze warunków. Jeśli większość z poniższych pasuje do Twojej sytuacji, zacznij od pollingu i sięgaj po webhooks dopiero wtedy, gdy konkretny problem Cię do tego zmusi.

Kontrolujesz obie strony integracji. Gdy konsumentem jest dashboard lub narzędzie raportowe, które sam posiadasz i operujesz, polling daje przewidywalne i ograniczone zachowanie. Sam decydujesz o interwale, oknie czasowym i sposobie obsługi częściowych wyników.

Twój przypadek użycia jest retrospektywny. Tygodniowe raporty kampanii, miesięczne zadania agregacji i potoki uzgadniania danych nie zyskują na opóźnieniu poniżej minuty. Cron job uruchamiany co godzinę pod adresem /summary lub /breakdown/country jest architektonicznie prostszy i łatwiejszy w rozumowaniu niż stanowy receiver webhookowy z obsługą ponownych prób.

Nie masz publicznego endpointu do wystawienia. Webhooks wymagają URL dostępnego z infrastruktury Elido. Jeśli Twoja integracja działa w sieci prywatnej, funkcji Lambda bez stabilnego URL lub na lokalnej maszynie dewelopera, skonfigurowanie przychodzącego endpointu HTTPS może kosztować więcej złożonością operacyjną, niż warta jest uzyskana korzyść z opóźnienia.

Wolumen jest niski. Przy kilku tysiącach kliknięć dziennie różnica między czasem rzeczywistym a pięciominutowym opóźnieniem rzadko jest widoczna dla użytkowników końcowych. Polling jest prosty do zrozumienia, prosty w debugowaniu i nie generuje niespodzianek infrastrukturalnych.

Kiedy webhooks są właściwym wyborem

Webhooks mają sens, gdy opóźnienie jest wymaganiem produktowym, a nie miłym dodatkiem. W przypadku danych o kliknięciach przeczytaj tę sekcję jako opis planu na czas, gdy click.created zostanie wdrożone; do tego czasu te same przypadki użycia obsługuje krótki interwał pollingu endpointu clicks/recent.

Budujesz licznik na żywo lub UX w czasie rzeczywistym. Jeśli Twój produkt pokazuje użytkownikom liczbę kliknięć aktualizowaną w widoczny sposób w ciągu sekund od przekierowania, polling przy jakimkolwiek rozsądnym interwale będzie odczuwalnie przestarzały. Handler webhookowy inkrementujący licznik Redis przy zdarzeniach click.created i udostępniający go przez WebSocket lub SSE do frontendu to architektura, która osiągnie ten cel bez bombardowania API analitycznego, gdy to zdarzenie zostanie wdrożone. Obecnie najbliższym odpowiednikiem jest jednominutowy polling endpointu clicks/recent, który zatrzymuje się na ostatnim znanym wierszu.

Wzbogacasz rekordy CRM przy każdym kliknięciu. Powiązanie zdarzenia kliknięcia z rekordem kontaktu - identyfikacja, który konkretny potencjalny klient kliknął link w Twoim outboundowym e-mailu i aktualizacja jego osi czasu w CRM - jest wrażliwe na czas. Zanim polling się dogoni pięć minut później, przedstawiciel handlowy może już zadzwonić. Handler webhookowy uruchamiający aktualizację CRM w ciągu sekund od kliknięcia będzie właściwym narzędziem, gdy zdarzenia dla poszczególnych kliknięć będą dostępne; do tego czasu utrzymuj krótki interwał pollingu.

Prowadzisz workflow oparte na zdarzeniach. Workflow wyzwalane zdarzeniami kliknięć - wysyłanie e-maila z follow-upem po kliknięciu linku, aktualizacja segmentu subskrybenta, dekrementacja stanu magazynowego - są naturalnymi konsumentami webhooków. Planowane zdarzenie click.created ma zawierać wystarczająco danych, aby działać natychmiast, bez dodatkowego zapytania. Workflow wyzwalane zmianami linków, takimi jak link.created lub link.expired, mogą już korzystać z webhooków.

Masz stabilny, publicznie dostępny endpoint HTTPS. To jest warunek wstępny, od którego wszystko inne zależy. Jeśli masz już infrastrukturę produkcyjną przyjmującą przychodzące webhooks od innych dostawców (Stripe, GitHub, Twilio), dodanie Elido do tego samego receivera jest bezproblemowe.

Ukryte koszty webhooków

Webhooks brzmią prosto: serwer wysyła POST, Ty go obsługujesz. Rzeczywista powierzchnia implementacji jest większa.

Warstwowy diagram czterech bram, przez które przechodzi każde przychodzące dostarczenie webhooka: weryfikacja podpisu HMAC, 300-sekundowe okno powtórzeń, deduplikacja idempotentności po ID dostarczenia i dostępność przed zwróceniem 2xx.

Weryfikacja podpisu

Elido podpisuje każde dostarczenie webhooka przy użyciu HMAC-SHA256. Format podpisu to v1=HMAC-SHA256(secret, "${unix_timestamp}.${body}"), dostarczany w nagłówku X-Elido-Signature oraz jako alias w X-Webhook-Signature. Znacznik czasu jest wysyłany oddzielnie w X-Webhook-Timestamp.

Musisz zweryfikować ten podpis przed przetworzeniem ładunku; przewodnik po weryfikacji podpisów webhooków obejmuje również Python, Go i rotację sekretów. Receiver pomijający weryfikację przetworzy każdy POST docierający do endpointu, w tym sfałszowane żądania od każdego, kto odkryje URL Twojego webhooka.

Poniżej znajduje się minimalny handler Express w TypeScript, który weryfikuje podpis przed wykonaniem czegokolwiek z ładunkiem:

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

const app = express();

// Use raw body middleware - JSON parsers consume the stream before you can hash it
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");
  // Use timingSafeEqual to prevent timing-based enumeration
  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" });
  }

  // Reject payloads older than 5 minutes
  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 === "link.updated") {
    // Handle the link event
    console.log("link updated:", event.data);
  }

  // Always return 2xx promptly - do heavy processing async
  return res.status(200).json({ received: true });
});

Okno powtórek

Sprawdzenie znacznika czasu w powyższym przykładzie wymusza to, co dokumentacja Elido nazywa oknem powtórek. Bez niego atakujący, który przechwyci jeden ważny podpisany ładunek, może go odtwarzać w nieskończoność - podpis pozostaje ważny na zawsze, ponieważ jest obliczany na podstawie stałego znacznika czasu. Przy tym sprawdzeniu ładunek starszy niż pięć minut jest odrzucany niezależnie od tego, czy podpis jest ważny.

Ustaw tolerancję na coś, co Twoja infrastruktura może obsłużyć. Pięć minut to konwencjonalne domyślne ustawienie, zgodne z tym, co używa Stripe. Jeśli Twój receiver okazjonalnie schodzi offline na kilka minut podczas wdrożeń, to okno daje mu czas na powrót i nadal przetwarza dostarczenia w locie.

Ponowne próby i idempotentność

Elido ponawia nieudane dostarczenia według harmonogramu backoff: pierwsza ponowna próba następuje 5 minut po niepowodzeniu, a druga 15 minut później. Dostarczenie ma domyślnie łącznie 3 próby. Po 3 nieudanych próbach dostarczenie jest oznaczane jako trwale nieudane i pojawia się w rejestrze dostarczeń endpointu.

Oznacza to, że Twój receiver może otrzymać to samo zdarzenie więcej niż raz. Każde przetwarzanie mające efekty uboczne - zapis do bazy danych, wysłanie e-maila, aktualizacja licznika - musi być idempotentne. Nagłówek X-Webhook-Delivery zawiera stabilne ID dostarczenia, którego możesz użyć jako klucza idempotentności.

// Before processing, check whether this delivery has already been handled
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 });
}
// Mark as processed with a TTL that covers the retry window
await redis.set(`webhook:delivery:${deliveryId}`, "1", "EX", 3600);

Twój endpoint musi być wysoko dostępny

Okno ponownych prób jest skończone. Jeśli Twój receiver jest niedostępny przez ponad około 20 minut (5 + 15), dostarczenia wyczerpią swoje próby i trwale się nie powiodą. W przypadku zdarzeń, gdzie gwarantowane dostarczenie ma znaczenie - wzbogacanie CRM, hooki rozliczeniowe - Twoja infrastruktura receivera wymaga właściwej dostępności, a nie serwera hobbysty, który okazjonalnie się restartuje.

To jest najbardziej niedoceniany koszt webhooków dla zespołów nowych w obszarze przychodzącego HTTP. Polling degraduje się w sposób łagodny: jeśli zadanie pollingu się nie powiedzie, po prostu uruchomi się ponownie przy następnym interwale i dogoni zaległości. Receiver webhookowy, który jest niedostępny, trwale traci zdarzenia, chyba że masz strategię uzgadniania.

Ukryte koszty pollingu

Polling wygląda prosto z zewnątrz. Rzeczywiste koszty kumulują się w produkcji.

Opóźnienie jest głównym ograniczeniem. Cron job uruchamiany co pięć minut oznacza, że dane kliknięć są nieaktualne do pięciu minut. Dla większości retrospektywnych przypadków użycia jest to akceptowalne; dla czegokolwiek skierowanego do użytkownika - nie. Skracanie interwału pomaga, ale nie eliminuje opóźnienia, a bardzo krótkie interwały (poniżej minuty) zaczynają wyglądać bardziej jak bombardowanie API niż polling.

Zmarnowane żądania. Większość interwałów pollingu zwraca te same dane co poprzednie żądanie. Jeśli odpytujesz link o małym ruchu co minutę, a kliknięcia pojawiają się mniej więcej raz na godzinę, 59 na 60 żądań nie zwraca nic nowego. Te żądania nadal wliczają się do Twojego limitu szybkości API.

Limity szybkości. API Elido egzekwuje limity szybkości per-workspace dostosowane do poziomu rozliczeniowego. Zadanie pollingu uruchamiane często na wielu linkach w dużym workspace może trafić na te limity, szczególnie jeśli inna automatyzacja w tym samym workspace też wykonuje wywołania API. API zwraca 429 Too Many Requests z nagłówkiem X-RateLimit-Scope: workspace, gdy tak się dzieje.

Paginacja i pominięte zdarzenia. Endpoint /clicks/recent używa paginacji opartej na kursorze. Jeśli odpytujesz na podstawie stałego okna czasowego - ?from=<last_poll>&to=<now> - a wolumen w tym oknie przekracza rozmiar strony, pominiesz zdarzenia, chyba że będziesz podążać za next_cursor przez wszystkie strony. Implementacja pollingu, która nie obsługuje paginacji, będzie po cichu gubić dane pod obciążeniem.

Wzorzec hybrydowy

W przypadku większości wdrożeń produkcyjnych najlepsza odpowiedź nie jest ani/ani.

Diagram architektury hybrydowej: gdy planowane zdarzenie click.created zostanie wdrożone, Elido przekazuje zdarzenia kliknięć do handlera webhookowego jako podstawową ścieżkę czasu rzeczywistego, podczas gdy nocny polling /summary i /timeseries uzgadnia sumy w Twoim magazynie i wypełnia luki po oknie ponownych prób.

Gdy click.created zostanie wdrożone, używaj webhooków jako głównej ścieżki dla reakcji w czasie rzeczywistym: aktualizacje CRM, liczniki na żywo, workflow oparte na zdarzeniach. Opóźnienie jest niskie; narzut operacyjny jest do opanowania, jeśli masz już infrastrukturę przychodzącego HTTPS. Do tego czasu szybką ścieżką jest krótki interwał pollingu endpointu clicks/recent, a opisana poniżej ścieżka uzgadniania działa tak samo w oparciu o niego.

Używaj pollingu jako tygodniowego lub dziennego przebiegu uzgadniania: pobierz pełną serię czasową za poprzedni tydzień, porównaj sumy z tym, co zarejestrował Twój handler webhookowy, i zidentyfikuj wszelkie luki. To wychwytuje dostarczenia, które wyczerpały okno ponownych prób podczas awarii, zdarzenia, które pojawiły się nie po kolei, i wszelkie rozbieżności między Twoim lokalnym stanem a źródłem prawdy Elido.

API analityczne dobrze nadaje się do tej roli. Endpoint /summary zwraca zagregowane sumy dla zakresu dat w jednym zapytaniu; endpoint /timeseries zwraca dzienne zasobniki. Zadanie uzgadniania uruchamiane raz na noc i porównujące zarejestrowane liczniki kliknięć Twojego CRM z podsumowaniem API dla tego samego okna może wykrywać problemy z integralnością danych, zanim staną się problemami widocznymi dla klientów.

Cron pollingu w Pythonie

Polling jest obecnie punktem wyjścia dla każdej integracji kliknięć i będzie działać także po wdrożeniu webhooków dla poszczególnych kliknięć. Poniżej znajduje się minimalna implementacja wykorzystująca bibliotekę schedule, która wywołuje clicks/recent co pięć minut (szczegółowe omówienie parametrów kursora, daty i strefy czasowej znajduje się w przewodniku po API analitycznym linków):

import schedule
import time
import requests
import os

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

# Newest click already processed, kept across runs. clicks/recent returns
# newest first and next_cursor pages to older rows, so each run reads from
# the top and stops when it reaches this row.
_last_seen = None

def poll_recent_clicks():
    global _last_seen
    params = {"limit": 100}
    newest = None
    done = False

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

        for click in body.get("items", []):
            key = (click["ts"], click["link_id"])
            if key == _last_seen:
                done = True
                break
            if newest is None:
                newest = key
            process_click(click)

        next_cursor = body.get("next_cursor")
        if not next_cursor:
            break
        params["cursor"] = next_cursor

    if newest:
        _last_seen = newest

def process_click(click: dict):
    # Replace with your actual processing logic
    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()  # run once on startup to catch up
    while True:
        schedule.run_pending()
        time.sleep(10)

W wdrożeniu produkcyjnym zastąp print swoim rzeczywistym ujściem - zapisem do bazy danych, wywołaniem API CRM, publikacją do kolejki wiadomości - i dodaj obsługę błędów z wykładniczym backoffem wokół wywołania requests.get.

Filtrowanie botów i co to oznacza dla Twojej integracji

Jeden szczegół, który dotyczy obu wzorców: warstwa przekierowań Elido filtruje kliknięcia botów, zanim zostaną zapisane. Żądania od Googlebot, Bingbot, Slackbot, monitorów czasu pracy, curl, bibliotek skryptowych i pustych User-Agentów nie są liczone jako kliknięcia, nie pojawiają się w wynikach API analitycznego i nie będą generować zdarzeń click.created, gdy te zostaną wdrożone.

Ma to znaczenie, ponieważ oznacza, że Twój handler webhookowy lub zadanie pollingu pracuje z liczbą ludzkich przekierowań, a nie surową liczbą żądań HTTP. Jeśli korelujesz dane kliknięć Elido z metrykami po stronie serwera - logami serwera aplikacji, logami dostępu CDN - oczekuj, że liczby Elido będą niższe. Rozbieżność to nie błąd; to filtr botów usuwający szum zanim dotrze do Ciebie.

Aby uzyskać więcej szczegółów na temat tego, co obejmuje filtr botów i jak skorer podejrzeń oznacza graniczny ruch, przewodnik po analityce zawiera pełny opis. Aby zapoznać się z właściwościami bezpieczeństwa schematu podpisywania webhooków - w tym formatem HMAC, powiązaniem ze znacznikiem czasu i tym, czemu zapobiega - odwołaj się do listy kontrolnej bezpieczeństwa.


Strona cennika zawiera zestawienie tego, które poziomy planów obejmują endpointy webhookowe i przy jakich limitach wolumenu dostarcze.

Powiązane wpisy na blogu

Wypróbuj Elido

Wklej URL, otrzymaj krótki link

Bez rejestracji. Link działa 30 dni. Zarejestruj się, aby zachować go na zawsze.

Za darmo, bez rejestracji · 2 dziennie

Wypróbuj Elido

Skracarka URL hostowana w UE: własne domeny, głęboka analityka i otwarte API. Darmowy plan - bez karty kredytowej.

Tagi
click tracking
webhooks
url shortener api
link analytics
api integration
event-driven
polling
real-time analytics

Czytaj dalej