-
-
Notifications
You must be signed in to change notification settings - Fork 28
Tracker Connections
c2c_tracking (repo c2corg/c2c_tracking) handles OAuth linking and activity sync with Strava, Garmin, Suunto, Decathlon, Polar and Coros. It runs as a sidecar next to c2c_ui on ui0/ui1, behind HAProxy at tracking.camptocamp.org (two replicas for redundancy, automatic failover via /health checks every 2s).
Auth: c2c_tracking doesn't issue its own JWTs - it verifies tokens signed by v6_api. TRACKING_JWT_SECRET_KEY must be set to the exact same value as v6_api's jwt_secret_key (shared HMAC secret). A mismatch fails every authenticated call (/users/:id/status, /users/:id/activities) with a real 401 - looks like a CORS problem from the frontend but isn't.
Sync, per vendor:
- Strava / Suunto / Decathlon / Coros: last ~30 activities pulled on account link, then a webhook fires on each new activity.
-
Garmin: no pull call exists; linking calls
garminApi.backfillActivities(days, ...), which asks Garmin to replay N days of history asynchronously via the normal webhook. - Polar: webhook-only, nothing pulls past activity history.
All vendors except Garmin silently drop any activity whose GPS track couldn't be parsed (only an info-level log, by design - no point storing a trackless activity). A parsing bug in a vendor's format can therefore cause total, silent data loss for that vendor until someone notices.
Two replicas, shared state: webhook subscription ids/secrets live in the shared DB, not per-instance. Only one replica needs to successfully register a subscription; either replica can then handle incoming webhooks. A "... couldn't be requested, maybe another webhook is already registered" warning on one replica at startup is expected noise if the other replica logs "Found matching ... webhook subscription" - not a sign of breakage.
Confirmed incident, Strava only: Strava changed its activity-streams response format at some point before 2025-09; the old parsing code threw on every fetch, so between then and v2.5.2 (deployed 2026-09) no Strava activity was synced at all, silently. Strava keeps full history server-side, so it's recoverable.
A backfill script exists per vendor (scripts/backfill-*.ts, driven by a shared scripts/backfill-common.ts):
BACKFILL_DELAY_MS=1000 npm run backfill:strava
npm run backfill:suunto
npm run backfill:decathlon
npm run backfill:coros
BACKFILL_GARMIN_DAYS=90 npm run backfill:garmin-
Strava / Suunto / Decathlon / Coros: re-syncs, for every user with a linked account, their most recent activities (each vendor's default page size, 30 for Strava - matches
MAX_ACTIVITIES_PER_USER, nothing further back is recoverable without adding pagination). Safe to re-run - deduped by vendor+activity id. -
Garmin: different shape - no synchronous "list activities" pull exists, only
garminApi.backfillActivities(), which asks Garmin to replay historical webhook notifications asynchronously.backfill:garminchunks the requested window (BACKFILL_GARMIN_DAYS, default 30) into Garmin's ~90-day-per-request limit; nothing is recovered synchronously, the data arrives via the normal webhook afterwards. - Suunto/Decathlon/Coros: built as general recovery tools - unlike Strava, no specific bug was confirmed for them. Worth running as a precaution, not because of a known incident.
- Polar: deliberately not built. Polar's AccessLink API only exposes data from the 90 days following a user's original account link, and only new data at that - already-elapsed history isn't retrievable through any endpoint, including their "transactions" flow. For anyone linked more than 90 days ago, that window has already closed for good, so a backfill script's value here is narrow. Revisit only if it turns out to matter for a meaningful number of recently-linked users.
BACKFILL_DELAY_MS (default 1000ms) paces requests between users to stay under each vendor's rate limits.
Two distinct things have to both be current for a config change (e.g. the dns: block below) to actually take effect:
- The infra repo checkout on the production host needs
git pull- the compose file lives there, not in the tracking image. Pulling a new tracking image and recreating the container still uses whateverdocker-compose.ymlis already on disk. -
docker compose ... restartreuses the existing container as-is - it does not pick updns:or other container-level compose settings even if the file on disk is current. Usedocker compose ... up -d <service>to actually recreate it.
Bit us after adding a DNS-resolver bypass to tracking (Docker's embedded DNS couldn't resolve external hosts like api.strava.com) - one replica kept failing with getaddrinfo EAI_AGAIN until both steps were actually done. To verify it took effect:
docker compose -f composition/production/ui0/docker-compose.yml exec tracking cat /etc/resolv.conf
docker compose -f composition/production/ui0/docker-compose.yml exec tracking getent hosts www.strava.comc2c_tracking's version tag (e.g. v2.5.2) is cut from main at a point in time - anything merged to main afterwards (like a bugfix PR) sits unreleased until someone runs npm version and cuts a new tag, then the infra repo's compose file is bumped to that tag and the container is recreated. Seeing an error in prod that matches an already-merged fix almost always means "not deployed yet," not "the fix didn't work" - check git log <deployed-tag>..main on c2c_tracking before re-diagnosing.
c2c_ui's outing editor (MapInputRow.vue) only offers "pick a synced activity" when GET /users/:id/status succeeds and reports at least one vendor as configured; any error there (401, network, ...) used to be swallowed silently, so it fell back to local-GPX-only with zero indication anything was wrong. This is exactly what made the JWT-secret-mismatch incident above hard to notice from the frontend. Now logged via console.error at minimum - check the browser console before assuming "no trackers connected" when a user reports the option missing.
β°οΈ Production UI β’ Production API β’ Prod π§ββοΈ
- π Home
- π Changelog
- π οΈ Dev tips
- πͺ Production Recipes
- π Deployment
- π¨βπ Useful informations
- β οΈ Legacy