Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
telemetry.h
v0.1.0-build.85
Latest
Commit ede38f5
Sign in with GitHub to download. Each download is recorded with your GitHub username.
| 1 | |
| 2 | |
| 3 | |
| 4 | /* |
| 5 | * CKey telemetry core (specification §9, docs/adoption-and-telemetry.md). |
| 6 | * |
| 7 | * This module is optional and self-contained. It is built as its own library |
| 8 | * (ckey_telemetry); the scanner library does not depend on it and it does not |
| 9 | * call any other ckey_ function, so builds for OS vendors can leave it out. |
| 10 | * This header includes ckey/ckey.h only for CKEY_API and ckey_status. |
| 11 | * |
| 12 | * WHAT IT DOES |
| 13 | * It keeps aggregate counters in process memory and serializes them into the |
| 14 | * version-1 batch JSON. The core NEVER does networking: SDK wrappers take a |
| 15 | * batch, POST it to CKEY_TELEMETRY_ENDPOINT asynchronously, off the decode |
| 16 | * path, and report the HTTP result back with ckey_telemetry_batch_result(). |
| 17 | * The core never reads a clock either; every time is a caller-supplied |
| 18 | * monotonic millisecond value (the UTC day for a batch is also supplied). |
| 19 | * |
| 20 | * WHAT IS COLLECTED |
| 21 | * Session counts (started, succeeded, failed), the number of processed |
| 22 | * frames, decodes per profile, a histogram of time to first decode, a |
| 23 | * histogram of per-frame processing time, the SDK name and version, a coarse |
| 24 | * platform name and the UTC day the batch was cut. |
| 25 | * |
| 26 | * WHAT IS NEVER COLLECTED |
| 27 | * Payloads, URLs or any decoded content; user, device or installation IDs; |
| 28 | * advertising IDs or fingerprints; location; IP addresses; per-scan event |
| 29 | * histories. No API in this header accepts any of these. |
| 30 | * |
| 31 | * MODES (canonical numeric values shared by every SDK) |
| 32 | * 0 CKEY_TELEMETRY_OFF no counting, nothing sent. |
| 33 | * 1 CKEY_TELEMETRY_ON local counting and aggregate batches (DEFAULT). |
| 34 | * 2 CKEY_TELEMETRY_LOCAL local counting, never sent. |
| 35 | * Switching (ckey_telemetry_set_mode): |
| 36 | * any -> 0 purges every counter (totals, unsent delta, in-flight batch) |
| 37 | * and the open session. Recording calls become no-ops. |
| 38 | * 1 -> 2 keeps local totals; drops the unsent delta; the in-flight |
| 39 | * batch (if any) is discarded and its result is ignored. |
| 40 | * 2 -> 1 starts a new unsent delta at zero; counts made in mode 2 are |
| 41 | * never sent. |
| 42 | * 0 -> 1|2 starts counting from zero. |
| 43 | * same mode no effect. |
| 44 | * Any other value is rejected with CKEY_INVALID_ARGUMENT; the mode is left |
| 45 | * unchanged and is never silently reset to 1. |
| 46 | * |
| 47 | * THREADING |
| 48 | * All state is process-global and guarded by one internal mutex; every |
| 49 | * function may be called from any thread. Recording calls take the mutex and |
| 50 | * do a few saturating integer updates; they never allocate, block on I/O or |
| 51 | * serialize. Serialization happens only in take_batch/export_state, which |
| 52 | * wrappers call off the decode path. |
| 53 | * |
| 54 | * COUNTERS |
| 55 | * Counters saturate at UINT64_MAX instead of wrapping. A batch never carries |
| 56 | * a count above CKEY_TELEMETRY_MAX_COUNT or a sum above CKEY_TELEMETRY_MAX_SUM; |
| 57 | * any excess stays in the unsent delta and goes in a later batch. |
| 58 | */ |
| 59 | |
| 60 | "ckey/ckey.h" |
| 61 | |
| 62 | <stddef.h> |
| 63 | <stdint.h> |
| 64 | |
| 65 | |
| 66 | extern "C" { |
| 67 | |
| 68 | |
| 69 | "https://openckey.org/api/telemetry/v1/batch" |
| 70 | 1 |
| 71 | |
| 72 | /* Default sdk_version reported until ckey_telemetry_set_client() is called: |
| 73 | * the core's own version (ckey.h), which matches the schema's pattern. */ |
| 74 | |
| 75 | CKEY_SOFTWARE_VERSION |
| 76 | |
| 77 | |
| 78 | /* Schema limits for one batch. */ |
| 79 | 10000000ull |
| 80 | 1000000000000ull |
| 81 | 4096u |
| 82 | |
| 83 | /* A session with no decode fails once now - start >= this many ms. */ |
| 84 | 30000u |
| 85 | /* Minimum ms between the last reported batch result and the next batch. */ |
| 86 | 3600000u |
| 87 | /* An unsent delta must be this old before the first batch is cut from it. */ |
| 88 | 60000u |
| 89 | |
| 90 | 10u |
| 91 | 5u |
| 92 | |
| 93 | typedef enum ckey_telemetry_mode { |
| 94 | CKEY_TELEMETRY_OFF = 0, |
| 95 | CKEY_TELEMETRY_ON = 1, |
| 96 | CKEY_TELEMETRY_LOCAL = 2 |
| 97 | } ckey_telemetry_mode; |
| 98 | |
| 99 | /* profile_code values for ckey_telemetry_session_success(); they index |
| 100 | * ckey_telemetry_counters.decoded and map to the batch keys p0..p3. */ |
| 101 | typedef enum ckey_telemetry_profile { |
| 102 | CKEY_TELEMETRY_PROFILE_P0 = 0, /* "p0" */ |
| 103 | CKEY_TELEMETRY_PROFILE_P0_1 = 1, /* "p0_1" */ |
| 104 | CKEY_TELEMETRY_PROFILE_P1 = 2, /* "p1" */ |
| 105 | CKEY_TELEMETRY_PROFILE_P2 = 3, /* "p2" */ |
| 106 | CKEY_TELEMETRY_PROFILE_P3 = 4 /* "p3" */ |
| 107 | } ckey_telemetry_profile; |
| 108 | |
| 109 | /* Histogram buckets use INCLUSIVE upper edges: a value v goes in the first |
| 110 | * bucket i with v <= edge[i]; the last bucket holds everything above the |
| 111 | * ninth edge. So 25 ms is in time_to_decode bucket 0 and 26 ms in bucket 1. |
| 112 | * time_to_decode_ms: 25, 50, 100, 200, 400, 800, 1600, 3200, 6400, inf |
| 113 | * frame_ms: 2, 5, 10, 20, 40, 80, 160, 320, 640, inf |
| 114 | * Invariant: the buckets add up to count. */ |
| 115 | typedef struct ckey_telemetry_histogram { |
| 116 | uint64_t count; |
| 117 | uint64_t sum; |
| 118 | uint64_t buckets[CKEY_TELEMETRY_BUCKETS]; |
| 119 | } ckey_telemetry_histogram; |
| 120 | |
| 121 | typedef struct ckey_telemetry_counters { |
| 122 | uint64_t sessions_started; |
| 123 | uint64_t sessions_succeeded; |
| 124 | uint64_t sessions_failed; |
| 125 | uint64_t frames; |
| 126 | uint64_t decoded[CKEY_TELEMETRY_PROFILES]; /* indexed by ckey_telemetry_profile */ |
| 127 | ckey_telemetry_histogram time_to_decode_ms; |
| 128 | ckey_telemetry_histogram frame_ms; |
| 129 | } ckey_telemetry_counters; |
| 130 | |
| 131 | /* ---- Configuration --------------------------------------------------- */ |
| 132 | |
| 133 | /* Sets the mode (0, 1 or 2) and applies the switching table above. |
| 134 | * Returns CKEY_INVALID_ARGUMENT, mode unchanged, for any other value. */ |
| 135 | CKEY_API ckey_status ckey_telemetry_set_mode(int mode); |
| 136 | CKEY_API int ckey_telemetry_get_mode(void); |
| 137 | |
| 138 | /* Sets the client identity reported in batches. All three are required and |
| 139 | * validated together; on any error nothing changes. |
| 140 | * sdk: "c", "swift", "android", "web" or "dotnet" (default "c") |
| 141 | * sdk_version: ^0\.[0-9]+\.[0-9]+(-(build|dev)\.[0-9]{1,6})?$, at most 63 |
| 142 | * bytes (default CKEY_TELEMETRY_DEFAULT_VERSION) |
| 143 | * platform: "ios", "macos", "android", "web", "windows", "linux" or |
| 144 | * "other" (default "other") |
| 145 | * The identity is configuration, not a counter: mode changes keep it. */ |
| 146 | CKEY_API ckey_status ckey_telemetry_set_client(const char *sdk, const char *sdk_version, |
| 147 | const char *platform); |
| 148 | |
| 149 | /* ---- Recording (no-ops in mode 0) ------------------------------------- |
| 150 | * |
| 151 | * now_ms is any monotonic millisecond clock of the caller's choice, used |
| 152 | * consistently. A value earlier than the session start counts as 0 ms elapsed. |
| 153 | * |
| 154 | * Session state machine: |
| 155 | * IDLE --begin/frame/success--> OPEN --success--> DONE |
| 156 | * OPEN --end, begin, or (now - start >= 30 s at the next call)--> failed |
| 157 | * DONE --end--> IDLE (no further count); DONE --begin--> new OPEN session |
| 158 | * |
| 159 | * - session_begin: the host started scanning. An OPEN session is first ended |
| 160 | * as failed (the host restarted without a decode). started += 1. |
| 161 | * - frame: counts one processed frame and its processing time. If the OPEN |
| 162 | * session has timed out it is ended as failed. If no session is open (IDLE, |
| 163 | * or just timed out) a new session implicitly begins at now_ms. In DONE the |
| 164 | * frame is counted as a frame only: per the normative doc, a session |
| 165 | * succeeds at its FIRST valid decode and later frames/decodes of the same |
| 166 | * session are not counted, so a new session begins only with the next |
| 167 | * session_begin (or after session_end, at the next frame). |
| 168 | * - session_success: the first valid decode. Records time to decode |
| 169 | * (now_ms - session start), succeeded += 1, decoded[profile] += 1, and moves |
| 170 | * the session to DONE. A timed-out OPEN session is first ended as failed; |
| 171 | * with no OPEN session, one implicitly begins at now_ms (time to decode 0). |
| 172 | * In DONE it is ignored. Returns CKEY_INVALID_ARGUMENT, recording nothing, |
| 173 | * for a profile_code outside 0..4. |
| 174 | * - session_end: the host stopped scanning. An OPEN session fails; a DONE |
| 175 | * session just closes. |
| 176 | * Timeouts are detected lazily, at the next recording call. |
| 177 | * The outcome of a session is counted in the delta of the mode current when |
| 178 | * the outcome happens. */ |
| 179 | CKEY_API void ckey_telemetry_session_begin(uint64_t now_ms); |
| 180 | CKEY_API void ckey_telemetry_frame(uint64_t now_ms, uint32_t processing_ms); |
| 181 | CKEY_API ckey_status ckey_telemetry_session_success(uint64_t now_ms, uint32_t profile_code); |
| 182 | CKEY_API void ckey_telemetry_session_end(uint64_t now_ms); |
| 183 | |
| 184 | /* Local totals since counting started (modes 1 and 2; all zero in mode 0). |
| 185 | * CKEY_INVALID_ARGUMENT if out is NULL. */ |
| 186 | CKEY_API ckey_status ckey_telemetry_snapshot(ckey_telemetry_counters *out); |
| 187 | |
| 188 | /* ---- Batching (mode 1 only; the wrapper does the HTTP) ---------------- |
| 189 | * |
| 190 | * Wrapper loop, e.g. on a timer and when the app moves to the background: |
| 191 | * if (ckey_telemetry_should_send(now)) { |
| 192 | * ckey_telemetry_take_batch(utc_day, buf, sizeof buf, &len, &id); |
| 193 | * ... POST buf (Content-Type: application/json) asynchronously ... |
| 194 | * ckey_telemetry_batch_result(id, http_status_or_0, now_after); |
| 195 | * } |
| 196 | * Every taken batch must be reported exactly once; until then no new batch |
| 197 | * can be taken. */ |
| 198 | |
| 199 | /* Non-zero only when: mode is 1, the unsent delta is non-empty, no batch is |
| 200 | * in flight, at least CKEY_TELEMETRY_SEND_INTERVAL_MS have passed since the |
| 201 | * now_ms of the last ckey_telemetry_batch_result (whatever its outcome, so a |
| 202 | * failed send is retried an hour later and there is never more than one batch |
| 203 | * per hour; the first batch of a process has no such wait), and the delta has |
| 204 | * existed for |
| 205 | * at least CKEY_TELEMETRY_MIN_DELTA_AGE_MS (so the first batch of a process |
| 206 | * is not cut after a single scan; deltas restored by import or by a failed |
| 207 | * send are not subject to this age). */ |
| 208 | CKEY_API int ckey_telemetry_should_send(uint64_t now_ms); |
| 209 | |
| 210 | /* Serializes the unsent delta as the compact version-1 batch JSON (keys in |
| 211 | * the documented order, NUL-terminated) and moves it to "in flight". |
| 212 | * utc_day: "YYYY-MM-DD", a valid calendar date (supplied by the caller). |
| 213 | * out_json/cap: output buffer; out_json may be NULL only when cap is 0. |
| 214 | * out_len: receives strlen of the JSON (excluding the NUL). |
| 215 | * out_batch_id: receives a non-zero id for ckey_telemetry_batch_result. |
| 216 | * Returns CKEY_INSUFFICIENT_BUFFER with *out_len = required length (cap must |
| 217 | * be at least *out_len + 1) and nothing moved. Returns CKEY_OK with |
| 218 | * *out_len = 0 and *out_batch_id = 0 when there is nothing to send (mode is |
| 219 | * not 1, delta empty, or a batch already in flight). The cadence is NOT |
| 220 | * checked here; call ckey_telemetry_should_send first. Values above the schema |
| 221 | * maxima are clamped and the excess stays in the unsent delta. The body is |
| 222 | * always well under CKEY_TELEMETRY_MAX_BODY_BYTES. */ |
| 223 | CKEY_API ckey_status ckey_telemetry_take_batch(const char *utc_day, char *out_json, size_t cap, |
| 224 | size_t *out_len, uint64_t *out_batch_id); |
| 225 | |
| 226 | /* Reports the outcome of a taken batch. |
| 227 | * http_status 200..299: accepted; the batch is dropped. |
| 228 | * 429, 0 (network error, timeout, no connectivity), 1xx, 3xx, 5xx: kept; |
| 229 | * the batch is merged back into the unsent delta for a later attempt. |
| 230 | * 400, 413 and every other 4xx: rejected; the batch is dropped. |
| 231 | * Returns CKEY_INVALID_ARGUMENT, changing nothing, when batch_id is not the |
| 232 | * batch currently in flight (for example after a mode switch made it stale: |
| 233 | * such a batch is never merged back or resent) or http_status is outside |
| 234 | * 0..999. now_ms (same clock as the recording calls) starts the hourly |
| 235 | * cadence interval for every accepted report, success or failure. */ |
| 236 | CKEY_API ckey_status ckey_telemetry_batch_result(uint64_t batch_id, int http_status, uint64_t now_ms); |
| 237 | |
| 238 | /* ---- Optional persistence (host option, independent of the mode) ------- |
| 239 | * |
| 240 | * The state holds the local totals and the unsent delta. It never holds the |
| 241 | * mode, the client identity, the in-flight batch or the open session. Format |
| 242 | * (compact JSON, keys in this exact order): |
| 243 | * {"state":1,"totals":C,"unsent":C} |
| 244 | * where C is {"sessions":{...},"frames":N,"decoded":{...}, |
| 245 | * "time_to_decode_ms":{...},"frame_ms":{...}} with the batch field layout. |
| 246 | * |
| 247 | * export: NUL-terminated into out/cap; *len = strlen. CKEY_INSUFFICIENT_BUFFER |
| 248 | * with *len = required length when cap < *len + 1. In mode 0 it exports an |
| 249 | * all-zero state, so a host that stores it also forgets purged counts. |
| 250 | * |
| 251 | * import: ADDS the stored counts to the current ones (saturating); call it |
| 252 | * once, at startup. In mode 2 only the totals are imported (mode-2 counts are |
| 253 | * never sent). Rejected with CKEY_INVALID_ARGUMENT in mode 0, for NULL, and |
| 254 | * for anything that is not exactly the format above: unknown, missing or |
| 255 | * reordered keys, non-decimal or out-of-range numbers, buckets that do not add |
| 256 | * up to count, trailing data, or input over 16 KiB. Whitespace between tokens |
| 257 | * is allowed. On rejection the state is unchanged. Import never changes the |
| 258 | * mode and never re-enables sending. */ |
| 259 | CKEY_API ckey_status ckey_telemetry_export_state(char *out, size_t cap, size_t *len); |
| 260 | CKEY_API ckey_status ckey_telemetry_import_state(const char *json); |
| 261 | |
| 262 | /* TEST ONLY. Restores defaults: mode 1, default client, all counters, the |
| 263 | * delta, the in-flight batch, the session and the cadence cleared. Not for |
| 264 | * production use. */ |
| 265 | CKEY_API void ckey_telemetry_reset_for_testing(void); |
| 266 | |
| 267 | |
| 268 | } |
| 269 | |
| 270 | |
| 271 |