From 271a22a5d9524e251e51518018b4177c806fe5e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 29 Apr 2026 10:41:37 +0200 Subject: [PATCH 1/3] Add Anonymous Telemetry page Documents the optional weekly telemetry check-in shipped with LoopFollow: what gets sent, what doesn't, where it goes, how to opt out, and how often the check runs. New top-level Privacy section in the nav so users can find it without digging through FAQs. Implementation: loopandlearn/LoopFollow#626 --- docs/privacy/lf-telemetry.md | 112 +++++++++++++++++++++++++++++++++++ mkdocs.yml | 2 + 2 files changed, 114 insertions(+) create mode 100644 docs/privacy/lf-telemetry.md diff --git a/docs/privacy/lf-telemetry.md b/docs/privacy/lf-telemetry.md new file mode 100644 index 0000000..2dfed10 --- /dev/null +++ b/docs/privacy/lf-telemetry.md @@ -0,0 +1,112 @@ +--- +## *LoopFollow* Anonymous Telemetry + +*LoopFollow* can send a small anonymous report once a week so the +maintainers can see which app and *iOS* versions are still in active use. +The data helps make decisions like when it's safe to drop support for +older *iOS* releases. **No glucose data, no credentials, no logs leave +your device.** + +This is opt-out: the first time the app comes to the foreground after +installing or updating to a version that supports telemetry, you'll see +a one-time prompt to choose Yes or No. You can change your mind any +time in *Settings* > *General Settings* > *Diagnostics*. + +- - - + +## What is sent + +A typical check-in looks like this — you can see the exact JSON your +own install would send under *Settings* > *General Settings* > +*Diagnostics* > *What's sent*. + +| Field | Example | Notes | +|:--|:--|:--| +| App version | `6.0.7` | | +| Build number | `100` | | +| Build branch and commit | `dev`, `d691b34` | The git branch and short commit SHA of the build. | +| Build date | `2026-04-15` | | +| TestFlight or not | `true` / `false` | Whether the install came from TestFlight or a local Xcode build. | +| Install ID | random UUID | Generated locally on first launch. Has no link to your device, account, or hardware. | +| Instance | `LoopFollow` / `LoopFollow_2` / ... | If you have multiple *LoopFollow* installs side by side, this distinguishes them. | +| Device | `iPhone15,2` | Apple's hardware identifier (not the marketing name). | +| Platform | `iOS` / `iPadOS` / `macCatalyst` | | +| iOS version | `17.5` | | +| Time zone | `Europe/Stockholm` | Coarse — not GPS-precise. | +| Background-refresh method | `Silent Tune` | Which background-refresh strategy is selected. | +| Display units | `mg/dL` / `mmol/L` | | +| Remote-command type | `none` / `Loop APNS` / ... | Which remote-command path is configured. | +| Appearance | `dark` / `light` / `system` | | +| Calendar / Contact integrations | `true` / `false` | Whether those features are turned on. | +| Cold launches in past 7 days | `12` | A count of process restarts; high values can flag stability issues. | + +For your *Dexcom Share* and *Nightscout* setup, an **anonymized +identifier** is included only when those backends are configured — +specifically, a salted, truncated cryptographic hash of your *Dexcom* +username and your *Nightscout* host. The actual username, password, +URL, and API token never leave your device. + +The server adds two fields when it stores each report: + +* `receivedAt` — the time the report was received. +* `weekBucket` — an ISO-week label (e.g. `2026-W17`) used to deduplicate. + +- - - + +## What is **not** sent + +Specifically and explicitly: + +* No glucose values, insulin or carb data, treatments, or any other health data. +* No *Nightscout* URL or API token. +* No *Dexcom* credentials. (The username is replaced with an anonymized identifier — see above.) +* No remote-command secrets, no APNS keys. +* No GPS or location data. +* **No logs.** Logs are never sent automatically. The existing *Settings* > *Logs* sharing flow is unchanged and only triggered by you. + +The receiving server also does not log your IP address — the *NGINX* +edge zeroes the last octet of *IPv4* addresses and only retains the +`/64` prefix of *IPv6* addresses before anything is written to disk. + +- - - + +## Where it goes + +Reports are sent over *HTTPS* to **`https://lf.bjorkert.se/api/telemetry/checkin`**, +which is self-hosted by the maintainer. There is no third-party +analytics service involved. + +- - - + +## How to opt out + +The toggle lives at *Settings* > *General Settings* > *Diagnostics* > +*Send anonymous usage stats*. Turning it off is immediate and persistent. + +The *Diagnostics* section also has: + +* *What's sent* — renders the exact *JSON* your install would send right now, + with a *Copy* button if you want to inspect it more closely. +* *Privacy* — a shorter in-app version of this page. + +- - - + +## How often + +A check-in is sent at most once every 7 days, or once after the app's +build SHA changes (whichever fires first). The check runs on every app +launch — including silent-push wake-ups and background-app-refresh +launches — so the cadence stays honest even if you rarely foreground +the app. + +If a send fails (network error, the server is unreachable, etc.) the +last-sent timestamp is not updated, so the next launch tries again. + +- - - + +## For the curious + +The *iOS* implementation lives at +[`LoopFollow/Helpers/Telemetry.swift`](https://github.com/loopandlearn/LoopFollow/blob/dev/LoopFollow/Helpers/Telemetry.swift) +on *GitHub*. The receiving server is a small *.NET* service in a +private repository on the maintainer's account. diff --git a/mkdocs.yml b/mkdocs.yml index ea07f9d..cf71bf8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -196,6 +196,8 @@ nav: - Build: - 'LoopFollow Build': 'build/build-options.md' - 'LoopFollow Browser Build': 'build/lf-browser-build.md' +- Privacy: + - 'Anonymous Telemetry': 'privacy/lf-telemetry.md' - FAQs: - 'LoopFollow FAQs': 'faqs/lf-faqs.md' - 'Glossary': faqs/glossary.md From ea4bd3b3cdbf3675ffdb26e65cb27b97ac16d63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 29 Apr 2026 21:14:55 +0200 Subject: [PATCH 2/3] Telemetry: update for payload + cadence revision - Drop time-zone field. Replace hashed NS host / Dexcom username with usesNightscout / usesDexcom yes-no flags. Add followingApp and IDFV rows. Bucket field is now dayBucket (yyyy-MM-dd) instead of weekBucket. - Cadence section reflects the new 24h TaskScheduler-driven flow. --- docs/privacy/lf-telemetry.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/privacy/lf-telemetry.md b/docs/privacy/lf-telemetry.md index 2dfed10..7a724c2 100644 --- a/docs/privacy/lf-telemetry.md +++ b/docs/privacy/lf-telemetry.md @@ -1,7 +1,7 @@ --- ## *LoopFollow* Anonymous Telemetry -*LoopFollow* can send a small anonymous report once a week so the +*LoopFollow* can send a small anonymous report once a day so the maintainers can see which app and *iOS* versions are still in active use. The data helps make decisions like when it's safe to drop support for older *iOS* releases. **No glucose data, no credentials, no logs leave @@ -29,10 +29,13 @@ own install would send under *Settings* > *General Settings* > | TestFlight or not | `true` / `false` | Whether the install came from TestFlight or a local Xcode build. | | Install ID | random UUID | Generated locally on first launch. Has no link to your device, account, or hardware. | | Instance | `LoopFollow` / `LoopFollow_2` / ... | If you have multiple *LoopFollow* installs side by side, this distinguishes them. | +| IDFV | Apple per-vendor UUID | Apple's `identifierForVendor` — opaque, scoped to *LoopFollow*'s bundle prefix, and resets when all of this developer's apps are removed from the device. | | Device | `iPhone15,2` | Apple's hardware identifier (not the marketing name). | | Platform | `iOS` / `iPadOS` / `macCatalyst` | | | iOS version | `17.5` | | -| Time zone | `Europe/Stockholm` | Coarse — not GPS-precise. | +| Following app | `Loop` / `Trio` / ... | Which closed-loop app *LoopFollow* is following, if known. Field is omitted when not yet detected. | +| Uses *Dexcom* | `true` / `false` | Whether *Dexcom Share* is configured. **No username or password.** | +| Uses *Nightscout* | `true` / `false` | Whether a *Nightscout* site is configured. **No URL or API token.** | | Background-refresh method | `Silent Tune` | Which background-refresh strategy is selected. | | Display units | `mg/dL` / `mmol/L` | | | Remote-command type | `none` / `Loop APNS` / ... | Which remote-command path is configured. | @@ -40,16 +43,10 @@ own install would send under *Settings* > *General Settings* > | Calendar / Contact integrations | `true` / `false` | Whether those features are turned on. | | Cold launches in past 7 days | `12` | A count of process restarts; high values can flag stability issues. | -For your *Dexcom Share* and *Nightscout* setup, an **anonymized -identifier** is included only when those backends are configured — -specifically, a salted, truncated cryptographic hash of your *Dexcom* -username and your *Nightscout* host. The actual username, password, -URL, and API token never leave your device. - The server adds two fields when it stores each report: * `receivedAt` — the time the report was received. -* `weekBucket` — an ISO-week label (e.g. `2026-W17`) used to deduplicate. +* `dayBucket` — a date label (e.g. `2026-04-29`) used to deduplicate. - - - @@ -58,8 +55,9 @@ The server adds two fields when it stores each report: Specifically and explicitly: * No glucose values, insulin or carb data, treatments, or any other health data. -* No *Nightscout* URL or API token. -* No *Dexcom* credentials. (The username is replaced with an anonymized identifier — see above.) +* No *Nightscout* URL or API token. Only a yes/no flag for whether *Nightscout* is configured. +* No *Dexcom* credentials. Only a yes/no flag for whether *Dexcom Share* is configured. +* No time zone. * No remote-command secrets, no APNS keys. * No GPS or location data. * **No logs.** Logs are never sent automatically. The existing *Settings* > *Logs* sharing flow is unchanged and only triggered by you. @@ -93,14 +91,16 @@ The *Diagnostics* section also has: ## How often -A check-in is sent at most once every 7 days, or once after the app's -build SHA changes (whichever fires first). The check runs on every app -launch — including silent-push wake-ups and background-app-refresh -launches — so the cadence stays honest even if you rarely foreground -the app. +A check-in is sent at most once every 24 hours, or once after the app's +build SHA changes (whichever fires first). It runs in the background +while the app is active or refreshing in the background — *LoopFollow* +schedules the next check-in based on when the last one was actually +sent, so a relaunch a few hours after the previous send simply waits +out the remainder of the 24 hours. If a send fails (network error, the server is unreachable, etc.) the -last-sent timestamp is not updated, so the next launch tries again. +last-sent timestamp is not updated, so the next scheduled check tries +again. - - - From 33256efaee0c115a6fcd2652973c8bec8870ca5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 29 Apr 2026 21:19:41 +0200 Subject: [PATCH 3/3] Telemetry: drop Install ID row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit clientId is no longer in the payload — IDFV + instance covers the same "identify this install" purpose. --- docs/privacy/lf-telemetry.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/privacy/lf-telemetry.md b/docs/privacy/lf-telemetry.md index 7a724c2..87f66e3 100644 --- a/docs/privacy/lf-telemetry.md +++ b/docs/privacy/lf-telemetry.md @@ -27,9 +27,8 @@ own install would send under *Settings* > *General Settings* > | Build branch and commit | `dev`, `d691b34` | The git branch and short commit SHA of the build. | | Build date | `2026-04-15` | | | TestFlight or not | `true` / `false` | Whether the install came from TestFlight or a local Xcode build. | -| Install ID | random UUID | Generated locally on first launch. Has no link to your device, account, or hardware. | | Instance | `LoopFollow` / `LoopFollow_2` / ... | If you have multiple *LoopFollow* installs side by side, this distinguishes them. | -| IDFV | Apple per-vendor UUID | Apple's `identifierForVendor` — opaque, scoped to *LoopFollow*'s bundle prefix, and resets when all of this developer's apps are removed from the device. | +| IDFV | Apple per-vendor UUID | Apple's `identifierForVendor` — opaque, scoped to *LoopFollow*'s bundle prefix, and resets when all of this developer's apps are removed from the device. Combined with the *Instance* field above, identifies a specific install. | | Device | `iPhone15,2` | Apple's hardware identifier (not the marketing name). | | Platform | `iOS` / `iPadOS` / `macCatalyst` | | | iOS version | `17.5` | |