Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
adoption-and-telemetry.md
v0.1.0-build.85
Latest
Commit ede38f5
Sign in with GitHub to download. Each download is recorded with your GitHub username.
| 1 | # Adoption and telemetry |
| 2 | |
| 3 | This document is normative for every CKey SDK and for the openckey.org ingestion service. It implements specification §9. |
| 4 | |
| 5 | ## Modes |
| 6 | |
| 7 | | Value | Name | Counts locally | Sends to openckey.org | |
| 8 | |---|---|---|---| |
| 9 | | `0` | `CKEY_TELEMETRY_OFF` | no | no | |
| 10 | | `1` | `CKEY_TELEMETRY_ON` | yes | yes, aggregate batches (**default**) | |
| 11 | | `2` | `CKEY_TELEMETRY_LOCAL` | yes | never | |
| 12 | |
| 13 | Every SDK uses these numbers. Each language may also offer named constants. Setting any other value is a configuration error: `CKEY_INVALID_ARGUMENT` in C, and the language's own error elsewhere. The mode is never silently reset to 1. |
| 14 | |
| 15 | Opting out takes one line, for example `CKey.telemetry = 0` (Swift, Kotlin, JS) or `ckey_telemetry_set_mode(0)` (C). |
| 16 | |
| 17 | ### Switching modes |
| 18 | |
| 19 | | From → to | Effect | |
| 20 | |---|---| |
| 21 | | any → `0` | Stops counting. **Purges** all counters, including unsent ones. Nothing more is sent. | |
| 22 | | `1` → `2` | Keeps local totals. **Drops** the unsent delta, so it can never be sent later. If a batch is in flight, its result is ignored. | |
| 23 | | `2` → `1` | Starts a new unsent delta at zero. Counts made while in mode 2 are never sent. | |
| 24 | | `0` → `1` or `2` | Starts counting from zero. | |
| 25 | |
| 26 | Persisting counters across app restarts is a separate host option (`ckey_telemetry_export_state` / `ckey_telemetry_import_state`). It is off unless the host stores the exported state. Importing never re-enables sending, and the mode is not part of the state. |
| 27 | |
| 28 | ## What a scan attempt is |
| 29 | |
| 30 | In continuous camera mode, the unit is a **scan session**, not a frame: |
| 31 | |
| 32 | - **Starts:** when the host starts scanning (`ckey_telemetry_session_begin`), for example when the camera view appears. |
| 33 | - **Succeeds:** at the first valid decode (`ckey_telemetry_session_success`). Later frames in the same session are not counted. |
| 34 | - **Fails:** when the host stops scanning without a decode (`ckey_telemetry_session_end`), or when 30 seconds pass with no decode. On timeout, the next frame implicitly starts a new session. |
| 35 | - **Frames:** processed frames are counted separately (`ckey_telemetry_frame`), together with their processing time. |
| 36 | |
| 37 | ## Collected fields |
| 38 | |
| 39 | Only counts and coarse durations are collected: |
| 40 | |
| 41 | - session counts (started, succeeded, failed); |
| 42 | - the number of frames processed; |
| 43 | - the number of decodes by profile; |
| 44 | - histograms of time to first decode and of per-frame processing time; |
| 45 | - the SDK name and version; |
| 46 | - a coarse platform name; |
| 47 | - the UTC day the batch was cut. |
| 48 | |
| 49 | The SDK **never** collects: |
| 50 | |
| 51 | - payloads, URLs or any decoded content; |
| 52 | - user, device or installation IDs; |
| 53 | - advertising IDs or fingerprints; |
| 54 | - location; |
| 55 | - IP addresses (beyond what the network itself exposes); |
| 56 | - per-scan event histories. |
| 57 | |
| 58 | Telemetry never runs on the decode path. Recording is a few atomic counter updates. Sending is asynchronous, batched, and never blocks or delays scanning. A failed send keeps the delta for the next attempt, and an offline device simply accumulates counts. |
| 59 | |
| 60 | ### Send cadence |
| 61 | |
| 62 | At most one batch per hour per app process, and only when there is something to send. The SDK also tries a send when the app moves to the background. One request carries one batch, so there is never one request per scan. |
| 63 | |
| 64 | ## Batch schema, version 1 |
| 65 | |
| 66 | `POST https://openckey.org/api/telemetry/v1/batch` with `Content-Type: application/json` and a body of at most 4,096 bytes: |
| 67 | |
| 68 | ```json |
| 69 | { |
| 70 | "schema": 1, |
| 71 | "sdk": "swift", |
| 72 | "sdk_version": "0.1.0-build.61", |
| 73 | "platform": "ios", |
| 74 | "day": "2026-11-05", |
| 75 | "sessions": { "started": 12, "succeeded": 10, "failed": 2 }, |
| 76 | "frames": 3120, |
| 77 | "decoded": { "p0": 7, "p0_1": 1, "p1": 1, "p2": 1, "p3": 0 }, |
| 78 | "time_to_decode_ms": { "count": 10, "sum": 4210, "buckets": [0, 1, 3, 4, 1, 1, 0, 0, 0, 0] }, |
| 79 | "frame_ms": { "count": 3120, "sum": 40110, "buckets": [10, 400, 2000, 600, 100, 10, 0, 0, 0, 0] } |
| 80 | } |
| 81 | ``` |
| 82 | |
| 83 | Field rules (the service rejects anything else with `400`): |
| 84 | |
| 85 | | Field | Rule | |
| 86 | |---|---| |
| 87 | | `schema` | exactly `1` | |
| 88 | | `sdk` | one of `c`, `swift`, `android`, `web`, `dotnet` | |
| 89 | | `sdk_version` | matches `^0\.[0-9]+\.[0-9]+(-(build\|dev)\.[0-9]{1,6})?$` | |
| 90 | | `platform` | one of `ios`, `macos`, `android`, `web`, `windows`, `linux`, `other` | |
| 91 | | `day` | `YYYY-MM-DD`, at most 2 days in the future and at most 35 days in the past (server clock) | |
| 92 | | `sessions` | object with exactly `started`, `succeeded` and `failed` | |
| 93 | | `frames` | integer | |
| 94 | | `decoded` | object with exactly `p0`, `p0_1`, `p1`, `p2` and `p3` | |
| 95 | | `time_to_decode_ms` and `frame_ms` | object with exactly `count`, `sum` and `buckets` (10 integers) | |
| 96 | |
| 97 | - Every count is an integer from 0 to 10,000,000. A `sum` is an integer from 0 to 10<sup>12</sup>. |
| 98 | - Bucket counts must add up to `count`. |
| 99 | - Unknown fields, at any depth, are rejected. |
| 100 | |
| 101 | Histogram bucket upper edges, in ms (the last bucket is everything above): |
| 102 | |
| 103 | | Histogram | Edges | |
| 104 | |---|---| |
| 105 | | `time_to_decode_ms` | 25, 50, 100, 200, 400, 800, 1600, 3200, 6400, ∞ | |
| 106 | | `frame_ms` | 2, 5, 10, 20, 40, 80, 160, 320, 640, ∞ | |
| 107 | |
| 108 | The service replies `204` when it accepts the batch, `400` for an invalid one, `413` if the body is too large, and `429` when rate limited. SDKs drop a batch after a `400` or `413`, and keep it after a `429`, `5xx` or network error. |
| 109 | |
| 110 | ## Service obligations |
| 111 | |
| 112 | - **Storage:** aggregates only, summed per (day, sdk, sdk_version, platform). Raw request bodies are not stored. |
| 113 | - **IP addresses:** |
| 114 | - not written to application logs or the database; |
| 115 | - rate limiting uses an in-memory key derived from the IP with a random salt that rotates daily; |
| 116 | - nothing derived from the IP is persisted. |
| 117 | - **Retention:** daily aggregates are kept for 400 days. |
| 118 | - **Abuse limits:** per-key rate limits, a per-day cap on accepted batches, and size limits. Counts can still be inflated by a determined sender; see below. |
| 119 | - **Reporting:** the public summary (`GET /api/telemetry/v1/summary`) labels its figures *reported by SDKs that left telemetry on*. These are not total usage, unique users or worldwide scans. |
| 120 | |
| 121 | ## Integrity caveat |
| 122 | |
| 123 | Anonymous counters from open-source SDKs are unauthenticated. They can be spoofed, replayed or inflated, and without identifiers they cannot be deduplicated. Treat the figures as a coarse signal, and publish the methodology alongside them. |
| 124 | |
| 125 | ## Other adoption evidence |
| 126 | |
| 127 | These are reported separately and never merged into one "users" number: |
| 128 | |
| 129 | - package downloads (not the same as active installs); |
| 130 | - GitHub activity; |
| 131 | - voluntarily registered apps and businesses; |
| 132 | - opt-in redirect counts from openckey.org short links. |
| 133 | |
| 134 | ## Privacy disclosures |
| 135 | |
| 136 | Integrators must disclose telemetry according to their own app-store and legal requirements. For example, Apple's privacy manifest lists "Product Interaction → Analytics, not linked to the user, not used for tracking" while mode 1 is on. Builds for OS vendors can leave the telemetry module out entirely: the scanner library does not depend on it. |