Webhooks er en metode for å utføre funksjoner når en spesifikk hendelse skjer i en ekstern tjeneste. Denne hendelsen kan være alt fra en slackmelding til en betalingsoppdatering i Stripe. Nylig var jeg med på å lage en sykkelkonkurranse, så her deler jeg om hvordan vi endte opp med å bruke webhooks med Strava.

Bakgrunn
Jeg var nylig med på å lage en sykkelkonkurranse, hvor vi hentet aktiviteter fra Strava og summerte distansen deltagerne syklet.
Her er et skjermbilde fra konkurransen:

Et stort spørsmål var: “når skal vi summere opp aktivitetene?”.
En første tanke var å summere idet brukeren besøker konkurransesiden. Men da hadde de kanskje ikke syklet siden sist de var innpå siden, og vi får unødvendige kall til Strava. Dette var et problem, siden vi hadde en begrensning i antall forespørsler til Strava per dag. I tillegg var det ikke brukervennlig å summere idet deltageren besøkte siden, på grunn av eventuell lastetid.
For å unngå lenger ventetid idet brukeren går inn, kunne vi i bakgrunnen heller hentet data på forhånd ved visse intervaller, såkalt polling. Så om vi polla hvert 5. minutt for nye data, ville brukeren i verste fall ikke vente så lenge med å se ny progresjon. Denne tilnærmingen hadde derimot ulempen at det kunne bli mange unødvendige kall til Strava. Vi kunne minsket antall kall ved å oppjustere tidsintervallet, som til 1 time, men dette blir igjen ikke brukervennlig. Etter en svett sykkeltur vil jo deltageren vite status med en gang!
Så … Finnes det en måte å vite om deltagerens aktivitet med engang hen har parkert sykkelen?
Webhooks i praksis
Det er nettopp dette webhooks er til for; utføre arbeid når en hendelse skjer. Vi kan bruke webhooks til å lytte på nye aktiviteter i Strava.
For å få til lytting på Strava-aktiviteter registrerer vi en lytter fra konkurranseapplikasjonen og til Strava. Så vil Strava fortelle appen vår når en deltager har utført en aktivitet, så vi kan oppdatere deltagerinfo.
Registrere en webhook
Registrering av webhooks gjøres vanligvis gjennom en “challenge”. Det innebærer at den eksterne tjenesten sender en forespørsel til deg, og du må svare med riktig melding.
Nedenfor beskriver jeg hvordan du kan sette opp webhooks med en enkel express-server.
Vi må først lage et endepunkt som kan motta challengen:
app.get("/webhook", (req, res) => {
const queryParams = req.query;
if (queryParams["hub.verify_token"] === STRAVA_WEBHOOK_VALIDATION_SECRET && queryParams["hub.mode"] === "subscribe") {
return res.status(200).json({ 'hub.challenge': queryParams["hub.challenge"] });
}
return res.status(401).send("Unauthorized");
});
Her tar vi imot en forespørsel og returnerer visse verdier Strava-challengen forventer for å registrere webhooken.
For å få Strava til å kontakte endepunktet i appen vår, må vi sende en engangs forespørsel til Strava.
Dette kan vi gjøre i Postman eller med curl i terminalen:
curl -X POST <https://www.strava.com/api/v3/push_subscriptions> \\
-F client_id=5 \\
-F client_secret=7b2946535949ae70f015d696d8ac602830ece412 \\
-F callback_url=http://eksempel-express-server.com/webhook \\
-F verify_token=STRAVA
Etter challengen er vellykket, vil Strava sende POST-forespørsler til oss hver gang det skjer endringer til Strava-appen vi har registrert.
Vi kan for eksempel lese av deltagerinfo, og oppdatere info hos den aktuelle deltageren:
app.post("/webhook", async (req, res) => {
const webhookEvent = req.body;
await updateCompetitionUser({ athleteId: webhookEvent.ownerId });
return res.status(200).send("OK");
});
Filtrere på hendelser
Det er flere ting som kan trigge et webhook-event, men det er ikke alle eventene vi bryr oss om — eller vi ønsker å gjøre ulike ting avhengig av hva som har skjedd. Dette kan vi lese av på webhook-eventet.
I vårt tilfelle bryr vi oss bare om oppdateringer på aktivitene, så vi kan filtrerere på object_type, som enten er “activity” eller “athlete”:
app.post("/webhook", async (req, res) => {
const webhookEvent = req.body;
if (webhookEvent.object_type === "activity") {
await updateCompetitionUser({ athleteId: webhookEvent.ownerId });
}
return res.status(200).send("OK");
});
Kombinere polling og webhooks
Jeg startet denne bloggposten med å sette polling og webhooks mot hverandre, men de er ikke gjensidig utelukkende. Siden vi ønsket å unngå å treffe Stravas rate-limit, oppdaterte vi ikke deltagernes progresjon direkte, men mellomlagret deltagernes id i en kø-tabell:
app.post("/webhook", async (req, res) => {
const webhookEvent = req.body;
if (webhookEvent.object_type === "activity") {
await enqueueAthleteId({ athleteId: webhookEvent.ownerId });
}
return res.status(200).send("OK");
});
Så polla vi fra tabellen hvert 5. minutt. Dette gjorde vi med en cron-jobb som kalte et endepunkt og oppdaterte progresjonen for deltagerne i kø-tabellen:
app.post("/dequeueAthleteIds", async (req, res) => {
const athleteIds = await getAthleteIdsFromQueue();
athleteIds.forEach(async (athleteId) => {
await updateCompetitionUser({ athleteId });
});
return res.status(200).send("OK");
});
På denne måten unngikk vi at samme deltager ble oppdatert oftere enn hvert 5. minutt, og med webhooks oppdaterte vi kun deltagerne som hadde utført en aktivitet. Begge deler minsket antall forespørsler til Strava. Deltageren måtte i verste fall vente 5 minutter, og det syns vi var en helt grei tradeoff.
For litt bedre brukervennlighet, viste vi også info til deltageren når aktiviteten var i køen, så brukeren fikk bekreftelse på at noe foregikk:

Avsluttende ord
Jeg håper denne posten har hjulpet deg med å forstå litt mer om hva webhooks er. Ved å kjenne til webhooks har du enda et verktøy til å lage hendelsesorienterte applikasjoner, både for å spare trafikk og ikke minst en bedre brukeropplevelse.
For mer detaljer om oppsett av webhooks med Strava, se til deres dokumentasjon: https://developers.strava.com/docs/webhooks/
Del kunnskapen
Har du en kollega som også hadde dratt nytte av denne artikkelen?
Skrevet av
Mer fra Fag i Bekk
Nå er du ved veis ende. Gå til forsiden hvis du vil ha mer faglig påfyll.
Til forsiden