10 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
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

Dwa zespoły budujące integracje na tym samym API skracacza URL często trafiają na zupełnie różne architektury. Jeden ustawia endpoint webhookowy i reaguje 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. 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/analytics/workspaces/{id}/clicks/recent lub /v1/analytics/workspaces/{id}/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 click.recorded na Twój endpoint HTTPS niedługo po przetworzeniu kliknięcia. Twój receiver obsługuje je, zwraca 2xx i dostarczenie jest zapisywane jako udane.

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.

Porownanie obok siebie: polling - Twoj cron job wysyla GET do API Elido i pobiera wiersze co piec minut; webhooks - serwer Elido wysyla POST click.recorded na Twoj endpoint HTTPS, ktory zwraca 2xx.

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.

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.recorded i udostępniający go przez WebSocket lub SSE do frontendu to architektura, która to osiąga bez bombardowania API analitycznego.

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 jest właściwym narzędziem.

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. Zdarzenie click.recorded zawiera wystarczająco danych, aby działać natychmiast, bez dodatkowego zapytania.

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 ktore przechodzi kazde przychodzace dostarczenie click.recorded: weryfikacja podpisu HMAC, 300-sekundowe okno powtorzenia, deduplikacja idempotentnosci po ID dostarczenia i dostepnosc przed zwroceniem 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-Webhook-Signature. Znacznik czasu jest wysyłany oddzielnie w X-Webhook-Timestamp. Oba są produkowane w services/webhook-dispatcher/internal/signing/hmac.go.

Musisz zweryfikować ten podpis przed przetworzeniem ładunku. 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 === "click.recorded") {
    // Handle the click event
    console.log("click recorded:", 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ść#

Webhook-dispatcher Elido ponawia nieudane dostarczenia według harmonogramu backoff: pierwsza próba po 1 minucie, druga po 5 minutach, trzecia po 15 minutach. Domyślna maksymalna liczba prób na dostarczenie wynosi 3, zgodnie ze schematem webhook_deliveries. Po 3 nieudanych próbach dostarczenie jest oznaczane jako trwale nieudane i pojawia się w panelu powiadomień.

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 21 minut (1 + 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: Elido przekazuje zdarzenia klikniec do handlera webhookowego jako podstawowa sciezka czasu rzeczywistego, podczas gdy nocny polling /summary i /timeseries uzgadnia sumy w Twoim magazynie i wypelnia luki po oknie ponownych prob.

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.

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#

Dla zespołów, które chcą zacząć od pollingu i przejść do webhooków później, poniżej znajduje się minimalna implementacja wykorzystująca bibliotekę schedule, która wywołuje /clicks/recent co pięć minut:

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}"}

# Track the cursor across poll intervals so we only fetch new clicks
_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:
            # Persist the current cursor for the next run
            if items:
                _cursor = None  # reset: next poll fetches from now
            break
        params["cursor"] = next_cursor

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: usługa edge-redirect Elido filtruje kliknięcia botów przed emisją zdarzeń kliknięć do potoku przetwarzania. Żądania od Googlebot, Bingbot, Slackbot, monitorów czasu pracy, curl, bibliotek skryptowych i pustych User-Agentów nie generują zdarzeń click.recorded i nie pojawiają się w wynikach API analitycznego.

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