A serverless application to collect anonymized product usage metrics for Garmin Connect Apps.
This application was written to provide better anonymized product usage metrics than the very limited stats Garmin App store provides to developers.
This application uses Cloudflare "Worker" compute and "D1 SQL" database storage. Cloudflare Worker and D1 are ideal for light telemetry, with up to 100,000 writes a day free, and competitive pricing beyond the free tier ($5/mnth for 50 million writes/month).
- Heartbeat tracking (
/heartbeat
) Devices report in with product code, firmware, software version, etc. - Active count (
/count
) Returns number of active devices in the last N seconds. - Metrics (
/metrics
) Query to provide usage metrics grouped by one or more dimension(s). Groups devices by one or more fields (part_num
,fw_version
,sw_version
,ciq_version
,country
,lang
,feat
) - Device list (
/list
) Diagnostic to list all active devices. - Authentication
- Client token (
X-Auth
) → required for client devices to send/heartbeat
. - Admin token (
X-Auth
) → required for reading out data/count
,/list
,/metrics
.
- Client token (
- GDPR-compliant
- Provides country-level aggregation; does not store any personally-identifiable information,
- Cache control
All responses are served with
Cache-Control: no-store, no-cache, must-revalidate
to help ensure queries never get stored / cached by any intermediary proxies.
item | description |
---|---|
- src/worker.ts | Worker implementation |
- wrangler.jsonc | Wrangler config |
- schema.sql | Database schema |
- README.md | Documentation |
See schema.sql
This worker can be deployed to any Cloudflare account; follow the instructions below to clone/fork and use this serverless application with your Garmin apps.
Node.js
brew install npm
Cloudflare Wrangler CLI
npm install -g wrangler
# Login to your Cloudflare account
wrangler login
# create a local copy of database
wrangler d1 execute infocal-db --file=./schema.sql
# start the worker (api endpoint http://localhost:8787)
wrangler dev --env dev
Note for testing, we use env.dev to pre-configure the test environment. See wrangler.jsonc for details of setup.
# Login to your Cloudflare account
wrangler login
# Create the remote (production) database
wrangler d1 execute infocal-db --file=./schema.sql --remote
# Set your production secrets in the cloudflare secrets manager
wrangler secret put CLIENT_TOKEN
wrangler secret put ADMIN_TOKEN
# Deploy your code to production
# (note for production we use the default environment)
wrangler deploy --env=""
NOTE: You must save a secure copy of your application secrets (CLIENT_TOKEN, ADMIN_TOKEN) as once written to the Worker Secrets they are encrypted and cannot be retrieved. You can only rotate the secret values.
export URL=http://localhost:8787
export CLIENT_TOKEN=dev-client-token
export ADMIN_TOKEN=dev-admin-token
export URL=https://infocal-worker.<your cloudflare subdomain>.workers.dev
export CLIENT_TOKEN=<your client secret>
export ADMIN_TOKEN=<your admin secret>
Simulate a client device sending a heartbeat:
curl -X POST "$URL/heartbeat?uid=test-123&part=F965&fw=15.23" -H "X-Auth: $CLIENT_TOKEN"
# response:
{ "ok": true, "lastSeen": 1725408000 }
Count the number of active devices within the last (window) seconds:
curl "$URL$/count?window=3600" -H "X-Auth: $ADMIN_TOKEN"
# response(JSON):
{ "timestamp": 1725408060, "window": 3600, "active": 123 }
curl "$URL$/count?window=3600&format=csv" -H "X-Auth: $ADMIN_TOKEN"
# response(CSV):
timestamp,window,active
1725408060,3600,123
Provide summary of usage, grouped by any dimension
curl "$URL/metrics?window=86400&groups=country,sw_version&format=csv" -H "X-Auth: $ADMIN_TOKEN"
# response(CSV)
country,sw_version,count
CA,2025.8.16,30
CA,2025.8.21,55
US,2025.8.16,2
US,2025.8.17,5
...
List raw table output (for debugging):
curl "$URL$/list?window=600&format=csv" -H "X-Auth: $ADMIN_TOKEN"
# response(CSV):
unique_id,first_seen,last_seen,part_num,fw_version,sw_version,country
d0983fdas,1725408000,1725408060,vivoactive,15.23,Unknown,CA
8moiuasf7,1725312456,1725398909,fenix8,10.5,2025.8.0,CA
...
Used to 'tail' the log file generated by the production worker:
wrangler tail infocal-worker
Very useful for confirming schema of local/remote database
wrangler d1 execute infocal-db \
--command "SELECT name, tbl_name, sql FROM sqlite_master;" \
[--local|--remote]
``
### Inspect table schema
Use to confirm current schema/changes changes
```sh
wrangler d1 execute infocal-db --command "PRAGMA table_info(heartbeats);" [--local|--remote]
Print the contents of table to console
wrangler d1 execute infocal-db --command "SELECT * FROM heartbeats LIMIT 100;" [--local|--remote]
Count the number of rows in table
wrangler d1 execute infocal-db \
--command "SELECT COUNT(*) AS count FROM heartbeats;" \
[--local|--remote]
Useful for deleting test entries from table
wrangler d1 execute infocal-db \
--command "DELETE FROM heartbeats WHERE unique_id = 'abc123';" \
[--local|--remote]
Maintain schema.sql in version control, and use this command to confirm/diff to local/remote, to ensure your migrations have been applied properly:
wrangler d1 export infocal-db --no-data --output schema.sql [--local|--remote]
Prepare a new (non-destructive) migration script under migrations/; run the migrations apply command to apply these migrations in order to the targeted database.
Wrangler will remember which migrations have already been applied, so next time it will only apply any new migrations.
WARNING: Test on local before applying to remote!
wrangler d1 migrations apply infocal-db [--local|--remote]
Re-export the schema.sql to check for correctness
wrangler d1 export infocal-db --no-data --output schema.sql [--local|--remote]
Deletes entries from the heartbeats table that have not been updated in 30 days.
NOTE: Batched as an atomic transaction:
- either all changes succeed (then COMMIT), or none of them apply (an error forces a rollback).
- SQLite flushes to disk at end of commit.
- If you delete 10,000 rows one by one without a transaction - that’s 10,000 commits, very slow!
- Ensures Consistency for other clients - particularly important for /list, /metrics endpoints
WARNING: you should test this (and any other destructive commands) on a local instance before applying to remote (production).
wrangler d1 execute infocal-db \
--remote \
--command "BEGIN; DELETE FROM heartbeats WHERE last_seen < strftime('%s','now') - 30*86400; COMMIT;" \
[--local|--remote]
wrangler d1 execute infocal-db --command "VACUUM;" [--local|--remote]
The following is an sql schema to restore rows accidentally deleted from heartbease, from the archive table.
INSERT INTO heartbeats
SELECT uniqu_id, first_seen, last_seen, part_num, fw_version, sw_version, ... <TODO> add all columns
FROM heartbeats_archive
WHERE deleted_at >= strftime('%s','now') - 3600; -- restore last hour
Used for adding/removing tables/columns/triggers etc. NOTE: Use wrangler migrations instead to manage schema changes in local/remote
In this example, the schema is modifying a trigger to the heartbeats table to automatically archive deleted rows to the heartbeats-archive table (for protection against accidental deletes)
wrangler d1 execute infocal-db --file=./schema/tr_heartbeats_delete_to_archive.sql [--local|--remote]
It is strongly recommended to store your CLIENT and ADMIN tokens in a persistent, safe place (such as a secure key/password manager).
For testing/admin, a good option is to store them in your machine keychain. Below are instructions for managing the token in the macOS keychain.
Stores tokens in the system Keychain and fetches them on demand:
# save (one-time)
security add-generic-password -a "$USER" -s infocal-admin -w '<ADMIN TOKEN HERE>' >/dev/null
security add-generic-password -a "$USER" -s infocal-client -w '<CLIENT TOKEN HERE>>' >/dev/null
# load into env when needed
export ADMIN_TOKEN="$(security find-generic-password -s infocal-admin -w 2>/dev/null)"
export CLIENT_TOKEN="$(security find-generic-password -s infocal-client -w 2>/dev/null)"
WARNING: Make sure you have a copy of your tokens elsewhere!
security delete-generic-password -s infocal-admin 2>/dev/null
security delete-generic-password -s infocal-client 2>/dev/null
- This app collects 'heartbeat' messages, containing anonymized product/version information.
- The heartbeat message contains: anonymized unique id, device part number, firmware version, software version, connect iq version, language and country code, and recently-used features.
- No peronallly-identifiable information or IP addresses are ever collected or stored.
- Data is processed and retained only for the purpose of anonymized product usage, product health, and service availability monitoring.