Viewing is open to everyone. Downloading and starring need a GitHub sign-in. Sign in with GitHub

telemetry.cpp

v0.1.0-build.85 Latest Commit ede38f5

Size
22 KB
Lines
631
Language
cpp
SHA-256
9e390a839dc3…
View on GitHub

Sign in with GitHub to download. Each download is recorded with your GitHub username.

1// CKey telemetry core. See include/ckey/telemetry.h and
2// docs/adoption-and-telemetry.md. Self-contained: no other ckey_ function is
3// used, no networking, no clock, no heap allocation.
4
5#include "ckey/telemetry.h"
6
7#include <cstring>
8#include <mutex>
9
10namespace {
11
12constexpr uint64_t kTtdEdges[CKEY_TELEMETRY_BUCKETS - 1] = {25, 50, 100, 200, 400, 800, 1600, 3200, 6400};
13constexpr uint64_t kFrameEdges[CKEY_TELEMETRY_BUCKETS - 1] = {2, 5, 10, 20, 40, 80, 160, 320, 640};
14constexpr uint64_t kMaxObservation = 0xffffffffull; // largest value one observation can add
15constexpr size_t kMaxImportBytes = 16384;
16constexpr size_t kMaxVersionBytes = 63;
17
18const char *const kSdks[] = {"c", "swift", "android", "web", "dotnet"};
19const char *const kPlatforms[] = {"ios", "macos", "android", "web", "windows", "linux", "other"};
20
21enum class Session { Idle, Open, Done };
22
23struct State {
24 int mode = CKEY_TELEMETRY_ON;
25 char sdk[8] = "c";
26 char sdk_version[kMaxVersionBytes + 1] = CKEY_TELEMETRY_DEFAULT_VERSION;
27 char platform[8] = "other";
28 ckey_telemetry_counters totals{};
29 ckey_telemetry_counters delta{}; // unsent, mode 1 only
30 ckey_telemetry_counters inflight{}; // taken, awaiting batch_result
31 bool inflight_active = false;
32 uint64_t inflight_id = 0;
33 uint64_t next_id = 1;
34 bool delta_has_since = false; // delta_since is valid
35 uint64_t delta_since = 0; // now_ms of the first count in the delta
36 bool delta_ready = false; // restored delta: exempt from the minimum age
37 bool has_last_attempt = false; // a batch result was reported
38 uint64_t last_attempt = 0; // now_ms of that report
39 Session session = Session::Idle;
40 uint64_t session_start = 0;
41};
42
43std::mutex g_mutex;
44State g_state;
45
46static_assert(sizeof(CKEY_TELEMETRY_DEFAULT_VERSION) <= kMaxVersionBytes + 1,
47 "CKEY_TELEMETRY_DEFAULT_VERSION is too long");
48
49inline void sat_add(uint64_t &a, uint64_t b) { a = (a > UINT64_MAX - b) ? UINT64_MAX : a + b; }
50
51void hist_record(ckey_telemetry_histogram &h, const uint64_t (&edges)[CKEY_TELEMETRY_BUCKETS - 1], uint64_t v) {
52 size_t i = 0;
53 while (i < CKEY_TELEMETRY_BUCKETS - 1 && v > edges[i]) ++i; // inclusive upper edges
54 sat_add(h.count, 1);
55 sat_add(h.sum, v);
56 sat_add(h.buckets[i], 1);
57}
58
59void hist_merge(ckey_telemetry_histogram &a, const ckey_telemetry_histogram &b) {
60 sat_add(a.count, b.count);
61 sat_add(a.sum, b.sum);
62 for (size_t i = 0; i < CKEY_TELEMETRY_BUCKETS; ++i) sat_add(a.buckets[i], b.buckets[i]);
63}
64
65void merge(ckey_telemetry_counters &a, const ckey_telemetry_counters &b) {
66 sat_add(a.sessions_started, b.sessions_started);
67 sat_add(a.sessions_succeeded, b.sessions_succeeded);
68 sat_add(a.sessions_failed, b.sessions_failed);
69 sat_add(a.frames, b.frames);
70 for (size_t i = 0; i < CKEY_TELEMETRY_PROFILES; ++i) sat_add(a.decoded[i], b.decoded[i]);
71 hist_merge(a.time_to_decode_ms, b.time_to_decode_ms);
72 hist_merge(a.frame_ms, b.frame_ms);
73}
74
75bool is_empty(const ckey_telemetry_counters &c) {
76 static const ckey_telemetry_counters zero{};
77 return std::memcmp(&c, &zero, sizeof c) == 0;
78}
79
80void clear_delta(State &s) {
81 s.delta = {};
82 s.delta_has_since = false;
83 s.delta_since = 0;
84 s.delta_ready = false;
85}
86
87void drop_inflight(State &s) {
88 s.inflight = {};
89 s.inflight_active = false;
90 s.inflight_id = 0;
91}
92
93// Applies f to the totals and, in mode 1, to the unsent delta. Caller holds
94// the lock and has checked that the mode is not 0.
95template <class F> void record(State &s, uint64_t now_ms, F f) {
96 f(s.totals);
97 if (s.mode == CKEY_TELEMETRY_ON) {
98 if (!s.delta_has_since) {
99 s.delta_has_since = true;
100 s.delta_since = now_ms;
101 }
102 f(s.delta);
103 }
104}
105
106inline uint64_t elapsed(uint64_t now_ms, uint64_t since) { return now_ms > since ? now_ms - since : 0; }
107
108void begin_session(State &s, uint64_t now_ms) {
109 s.session = Session::Open;
110 s.session_start = now_ms;
111 record(s, now_ms, [](ckey_telemetry_counters &c) { sat_add(c.sessions_started, 1); });
112}
113
114void fail_session(State &s, uint64_t now_ms) {
115 s.session = Session::Idle;
116 record(s, now_ms, [](ckey_telemetry_counters &c) { sat_add(c.sessions_failed, 1); });
117}
118
119// Ends an OPEN session as failed if it reached the timeout.
120void check_timeout(State &s, uint64_t now_ms) {
121 if (s.session == Session::Open && elapsed(now_ms, s.session_start) >= CKEY_TELEMETRY_SESSION_TIMEOUT_MS)
122 fail_session(s, now_ms);
123}
124
125// ---- Clamping a batch out of the delta --------------------------------
126
127uint64_t take_scalar(uint64_t &v) {
128 uint64_t t = v < CKEY_TELEMETRY_MAX_COUNT ? v : CKEY_TELEMETRY_MAX_COUNT;
129 v -= t;
130 return t;
131}
132
133// floor(a * b / c) for c > 0, exact for any 64-bit a and b, with a 128-bit
134// intermediate. The callers only ask for quotients that fit in 64 bits.
135// MSVC has no __int128, so it gets a portable multiply and long division.
136uint64_t mul_div(uint64_t a, uint64_t b, uint64_t c) {
137#if defined(__SIZEOF_INT128__) && !defined(CKEY_TELEMETRY_PORTABLE_MULDIV)
138 return static_cast<uint64_t>(static_cast<unsigned __int128>(a) * b / c);
139#else
140 const uint64_t a_lo = a & 0xFFFFFFFFu, a_hi = a >> 32, b_lo = b & 0xFFFFFFFFu, b_hi = b >> 32;
141 const uint64_t p0 = a_lo * b_lo, p1 = a_lo * b_hi, p2 = a_hi * b_lo, p3 = a_hi * b_hi;
142 const uint64_t mid = (p0 >> 32) + (p1 & 0xFFFFFFFFu) + (p2 & 0xFFFFFFFFu);
143 const uint64_t lo = (p0 & 0xFFFFFFFFu) | (mid << 32);
144 const uint64_t hi = p3 + (p1 >> 32) + (p2 >> 32) + (mid >> 32);
145 uint64_t q = 0, r = 0;
146 for (int i = 127; i >= 0; --i) {
147 const uint64_t bit = i >= 64 ? (hi >> (i - 64)) & 1u : (lo >> i) & 1u;
148 const bool carry = (r >> 63) != 0;
149 r = (r << 1) | bit;
150 q <<= 1;
151 if (carry || r >= c) {
152 r -= c;
153 q |= 1u;
154 }
155 }
156 return q;
157#endif
158}
159
160ckey_telemetry_histogram take_hist(ckey_telemetry_histogram &h) {
161 ckey_telemetry_histogram part{};
162 uint64_t eff = 0; // bucket total; equals count unless saturated
163 for (uint64_t b : h.buckets) sat_add(eff, b);
164 if (eff == 0) {
165 // Nothing observable; a stray sum without observations is not sendable.
166 h = {};
167 return part;
168 }
169 uint64_t n = eff < CKEY_TELEMETRY_MAX_COUNT ? eff : CKEY_TELEMETRY_MAX_COUNT;
170 if (mul_div(h.sum, n, eff) > CKEY_TELEMETRY_MAX_SUM)
171 n = mul_div(CKEY_TELEMETRY_MAX_SUM, eff, h.sum);
172 if (n == 0) n = 1; // unreachable with per-observation values <= 2^32-1
173 if (n == eff) {
174 part = h;
175 part.count = eff;
176 h = {};
177 return part;
178 }
179 uint64_t taken = 0;
180 for (size_t i = 0; i < CKEY_TELEMETRY_BUCKETS; ++i) {
181 part.buckets[i] = mul_div(h.buckets[i], n, eff);
182 taken += part.buckets[i];
183 }
184 for (size_t i = 0; i < CKEY_TELEMETRY_BUCKETS && taken < n; ++i) {
185 if (h.buckets[i] > part.buckets[i]) {
186 ++part.buckets[i];
187 ++taken;
188 }
189 }
190 part.count = n;
191 part.sum = mul_div(h.sum, n, eff);
192 if (part.sum > CKEY_TELEMETRY_MAX_SUM) part.sum = CKEY_TELEMETRY_MAX_SUM;
193 for (size_t i = 0; i < CKEY_TELEMETRY_BUCKETS; ++i) h.buckets[i] -= part.buckets[i];
194 h.sum -= part.sum;
195 h.count = eff - n;
196 return part;
197}
198
199ckey_telemetry_counters take_part(ckey_telemetry_counters &d) {
200 ckey_telemetry_counters p{};
201 p.sessions_started = take_scalar(d.sessions_started);
202 p.sessions_succeeded = take_scalar(d.sessions_succeeded);
203 p.sessions_failed = take_scalar(d.sessions_failed);
204 p.frames = take_scalar(d.frames);
205 for (size_t i = 0; i < CKEY_TELEMETRY_PROFILES; ++i) p.decoded[i] = take_scalar(d.decoded[i]);
206 p.time_to_decode_ms = take_hist(d.time_to_decode_ms);
207 p.frame_ms = take_hist(d.frame_ms);
208 return p;
209}
210
211// ---- Bounded JSON writer ----------------------------------------------
212
213struct Writer {
214 char *out;
215 size_t cap;
216 size_t len = 0;
217 void ch(char c) {
218 if (out && len + 1 < cap) out[len] = c;
219 ++len;
220 }
221 void str(const char *s) {
222 while (*s) ch(*s++);
223 }
224 void num(uint64_t v) {
225 char tmp[20];
226 size_t n = 0;
227 do {
228 tmp[n++] = static_cast<char>('0' + v % 10);
229 v /= 10;
230 } while (v);
231 while (n) ch(tmp[--n]);
232 }
233 void key_num(const char *key, uint64_t v) {
234 ch('"');
235 str(key);
236 str("\":");
237 num(v);
238 }
239 // NUL-terminates when it fits; returns whether it did.
240 bool finish() {
241 if (!out || len + 1 > cap) return false;
242 out[len] = '\0';
243 return true;
244 }
245};
246
247void write_hist(Writer &w, const char *name, const ckey_telemetry_histogram &h) {
248 w.ch('"');
249 w.str(name);
250 w.str("\":{");
251 w.key_num("count", h.count);
252 w.ch(',');
253 w.key_num("sum", h.sum);
254 w.str(",\"buckets\":[");
255 for (size_t i = 0; i < CKEY_TELEMETRY_BUCKETS; ++i) {
256 if (i) w.ch(',');
257 w.num(h.buckets[i]);
258 }
259 w.str("]}");
260}
261
262const char *const kProfileKeys[CKEY_TELEMETRY_PROFILES] = {"p0", "p0_1", "p1", "p2", "p3"};
263
264// The counter fields, in documented order, without the enclosing braces.
265void write_counters(Writer &w, const ckey_telemetry_counters &c) {
266 w.str("\"sessions\":{");
267 w.key_num("started", c.sessions_started);
268 w.ch(',');
269 w.key_num("succeeded", c.sessions_succeeded);
270 w.ch(',');
271 w.key_num("failed", c.sessions_failed);
272 w.str("},");
273 w.key_num("frames", c.frames);
274 w.str(",\"decoded\":{");
275 for (size_t i = 0; i < CKEY_TELEMETRY_PROFILES; ++i) {
276 if (i) w.ch(',');
277 w.key_num(kProfileKeys[i], c.decoded[i]);
278 }
279 w.str("},");
280 write_hist(w, "time_to_decode_ms", c.time_to_decode_ms);
281 w.ch(',');
282 write_hist(w, "frame_ms", c.frame_ms);
283}
284
285// ---- Strict, bounded state parser -------------------------------------
286
287struct Parser {
288 const char *s;
289 size_t n;
290 size_t i = 0;
291 void ws() {
292 while (i < n && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r')) ++i;
293 }
294 bool lit(const char *t) {
295 ws();
296 size_t l = std::strlen(t);
297 if (n - i < l || std::memcmp(s + i, t, l) != 0) return false;
298 i += l;
299 return true;
300 }
301 bool key(const char *name) {
302 ws();
303 if (i >= n || s[i] != '"') return false;
304 ++i;
305 size_t l = std::strlen(name);
306 if (n - i < l + 1 || std::memcmp(s + i, name, l) != 0 || s[i + l] != '"') return false;
307 i += l + 1;
308 return lit(":");
309 }
310 bool u64(uint64_t &v) {
311 ws();
312 if (i >= n || s[i] < '0' || s[i] > '9') return false;
313 if (s[i] == '0' && i + 1 < n && s[i + 1] >= '0' && s[i + 1] <= '9') return false; // leading zero
314 uint64_t r = 0;
315 while (i < n && s[i] >= '0' && s[i] <= '9') {
316 uint64_t d = static_cast<uint64_t>(s[i] - '0');
317 if (r > (UINT64_MAX - d) / 10) return false;
318 r = r * 10 + d;
319 ++i;
320 }
321 v = r;
322 return true;
323 }
324 bool key_u64(const char *name, uint64_t &v) { return key(name) && u64(v); }
325};
326
327bool parse_hist(Parser &p, const char *name, ckey_telemetry_histogram &h) {
328 if (!p.key(name) || !p.lit("{") || !p.key_u64("count", h.count) || !p.lit(",") || !p.key_u64("sum", h.sum) ||
329 !p.lit(",") || !p.key("buckets") || !p.lit("["))
330 return false;
331 for (size_t i = 0; i < CKEY_TELEMETRY_BUCKETS; ++i) {
332 if (i && !p.lit(",")) return false;
333 if (!p.u64(h.buckets[i])) return false;
334 }
335 if (!p.lit("]") || !p.lit("}")) return false;
336 // Consistency: buckets add up to count, and the sum is achievable.
337 uint64_t total = 0;
338 for (uint64_t b : h.buckets) {
339 if (total > UINT64_MAX - b) return false;
340 total += b;
341 }
342 if (total != h.count) return false;
343 // sum > count * kMaxObservation, without overflow: a product that does
344 // not fit in 64 bits exceeds every 64-bit sum.
345 if (h.count <= UINT64_MAX / kMaxObservation && h.sum > h.count * kMaxObservation) return false;
346 return true;
347}
348
349bool parse_counters(Parser &p, ckey_telemetry_counters &c) {
350 if (!p.lit("{") || !p.key("sessions") || !p.lit("{") || !p.key_u64("started", c.sessions_started) ||
351 !p.lit(",") || !p.key_u64("succeeded", c.sessions_succeeded) || !p.lit(",") ||
352 !p.key_u64("failed", c.sessions_failed) || !p.lit("}") || !p.lit(",") || !p.key_u64("frames", c.frames) ||
353 !p.lit(",") || !p.key("decoded") || !p.lit("{"))
354 return false;
355 for (size_t i = 0; i < CKEY_TELEMETRY_PROFILES; ++i) {
356 if (i && !p.lit(",")) return false;
357 if (!p.key_u64(kProfileKeys[i], c.decoded[i])) return false;
358 }
359 return p.lit("}") && p.lit(",") && parse_hist(p, "time_to_decode_ms", c.time_to_decode_ms) && p.lit(",") &&
360 parse_hist(p, "frame_ms", c.frame_ms) && p.lit("}");
361}
362
363// ---- Validation ---------------------------------------------------------
364
365bool in_list(const char *v, const char *const *list, size_t n) {
366 for (size_t i = 0; i < n; ++i)
367 if (std::strcmp(v, list[i]) == 0) return true;
368 return false;
369}
370
371inline bool is_digit(char c) { return c >= '0' && c <= '9'; }
372
373// ^0\.[0-9]+\.[0-9]+(-(build|dev)\.[0-9]{1,6})?$
374bool valid_version(const char *v) {
375 size_t len = strnlen(v, kMaxVersionBytes + 1);
376 if (len > kMaxVersionBytes) return false;
377 size_t i = 0;
378 if (v[i++] != '0' || v[i++] != '.') return false;
379 for (int part = 0; part < 2; ++part) {
380 size_t start = i;
381 while (is_digit(v[i])) ++i;
382 if (i == start) return false;
383 if (part == 0) {
384 if (v[i] != '.') return false;
385 ++i;
386 }
387 }
388 if (v[i] == '\0') return true;
389 if (v[i++] != '-') return false;
390 if (std::strncmp(v + i, "build.", 6) == 0) i += 6;
391 else if (std::strncmp(v + i, "dev.", 4) == 0) i += 4;
392 else return false;
393 size_t start = i;
394 while (is_digit(v[i])) ++i;
395 return i > start && i - start <= 6 && v[i] == '\0';
396}
397
398bool valid_day(const char *d) {
399 if (strnlen(d, 11) != 10) return false;
400 for (int i = 0; i < 10; ++i) {
401 if (i == 4 || i == 7) {
402 if (d[i] != '-') return false;
403 } else if (!is_digit(d[i])) {
404 return false;
405 }
406 }
407 int y = (d[0] - '0') * 1000 + (d[1] - '0') * 100 + (d[2] - '0') * 10 + (d[3] - '0');
408 int m = (d[5] - '0') * 10 + (d[6] - '0');
409 int day = (d[8] - '0') * 10 + (d[9] - '0');
410 static const int kDays[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
411 if (m < 1 || m > 12 || day < 1) return false;
412 bool leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
413 int max = kDays[m - 1] + ((m == 2 && leap) ? 1 : 0);
414 return day <= max;
415}
416
417void reset_counting(State &s) {
418 s.totals = {};
419 clear_delta(s);
420 drop_inflight(s);
421 s.session = Session::Idle;
422 s.session_start = 0;
423}
424
425} // namespace
426
427extern "C" {
428
429ckey_status ckey_telemetry_set_mode(int mode) {
430 if (mode != CKEY_TELEMETRY_OFF && mode != CKEY_TELEMETRY_ON && mode != CKEY_TELEMETRY_LOCAL)
431 return CKEY_INVALID_ARGUMENT;
432 std::lock_guard<std::mutex> lock(g_mutex);
433 State &s = g_state;
434 if (mode == s.mode) return CKEY_OK;
435 if (mode == CKEY_TELEMETRY_OFF || s.mode == CKEY_TELEMETRY_OFF) {
436 // any -> 0 purges; 0 -> 1|2 starts from zero (already zero).
437 reset_counting(s);
438 } else {
439 // 1 -> 2: keep totals, drop the delta and the in-flight batch.
440 // 2 -> 1: the delta starts at zero (it is always empty in mode 2).
441 clear_delta(s);
442 drop_inflight(s);
443 }
444 s.mode = mode;
445 return CKEY_OK;
446}
447
448int ckey_telemetry_get_mode(void) {
449 std::lock_guard<std::mutex> lock(g_mutex);
450 return g_state.mode;
451}
452
453ckey_status ckey_telemetry_set_client(const char *sdk, const char *sdk_version, const char *platform) {
454 if (!sdk || !sdk_version || !platform) return CKEY_INVALID_ARGUMENT;
455 if (!in_list(sdk, kSdks, sizeof kSdks / sizeof kSdks[0])) return CKEY_INVALID_ARGUMENT;
456 if (!in_list(platform, kPlatforms, sizeof kPlatforms / sizeof kPlatforms[0])) return CKEY_INVALID_ARGUMENT;
457 if (!valid_version(sdk_version)) return CKEY_INVALID_ARGUMENT;
458 std::lock_guard<std::mutex> lock(g_mutex);
459 State &s = g_state;
460 std::strcpy(s.sdk, sdk); // lengths bounded by the lists above
461 std::strcpy(s.sdk_version, sdk_version); // bounded by valid_version
462 std::strcpy(s.platform, platform);
463 return CKEY_OK;
464}
465
466void ckey_telemetry_session_begin(uint64_t now_ms) {
467 std::lock_guard<std::mutex> lock(g_mutex);
468 State &s = g_state;
469 if (s.mode == CKEY_TELEMETRY_OFF) return;
470 if (s.session == Session::Open) fail_session(s, now_ms);
471 begin_session(s, now_ms);
472}
473
474void ckey_telemetry_frame(uint64_t now_ms, uint32_t processing_ms) {
475 std::lock_guard<std::mutex> lock(g_mutex);
476 State &s = g_state;
477 if (s.mode == CKEY_TELEMETRY_OFF) return;
478 check_timeout(s, now_ms);
479 if (s.session == Session::Idle) begin_session(s, now_ms);
480 record(s, now_ms, [processing_ms](ckey_telemetry_counters &c) {
481 sat_add(c.frames, 1);
482 hist_record(c.frame_ms, kFrameEdges, processing_ms);
483 });
484}
485
486ckey_status ckey_telemetry_session_success(uint64_t now_ms, uint32_t profile_code) {
487 if (profile_code >= CKEY_TELEMETRY_PROFILES) return CKEY_INVALID_ARGUMENT;
488 std::lock_guard<std::mutex> lock(g_mutex);
489 State &s = g_state;
490 if (s.mode == CKEY_TELEMETRY_OFF) return CKEY_OK;
491 check_timeout(s, now_ms);
492 if (s.session == Session::Done) return CKEY_OK; // later decodes of the same session are not counted
493 if (s.session == Session::Idle) begin_session(s, now_ms);
494 uint64_t ttd = elapsed(now_ms, s.session_start);
495 s.session = Session::Done;
496 record(s, now_ms, [ttd, profile_code](ckey_telemetry_counters &c) {
497 sat_add(c.sessions_succeeded, 1);
498 sat_add(c.decoded[profile_code], 1);
499 hist_record(c.time_to_decode_ms, kTtdEdges, ttd);
500 });
501 return CKEY_OK;
502}
503
504void ckey_telemetry_session_end(uint64_t now_ms) {
505 std::lock_guard<std::mutex> lock(g_mutex);
506 State &s = g_state;
507 if (s.mode == CKEY_TELEMETRY_OFF) return;
508 if (s.session == Session::Open) fail_session(s, now_ms);
509 s.session = Session::Idle;
510}
511
512ckey_status ckey_telemetry_snapshot(ckey_telemetry_counters *out) {
513 if (!out) return CKEY_INVALID_ARGUMENT;
514 std::lock_guard<std::mutex> lock(g_mutex);
515 *out = g_state.totals; // zero in mode 0 (purged on entry, never recorded)
516 return CKEY_OK;
517}
518
519int ckey_telemetry_should_send(uint64_t now_ms) {
520 std::lock_guard<std::mutex> lock(g_mutex);
521 const State &s = g_state;
522 if (s.mode != CKEY_TELEMETRY_ON || s.inflight_active || is_empty(s.delta)) return 0;
523 if (s.has_last_attempt && elapsed(now_ms, s.last_attempt) < CKEY_TELEMETRY_SEND_INTERVAL_MS) return 0;
524 if (!s.delta_ready && s.delta_has_since && elapsed(now_ms, s.delta_since) < CKEY_TELEMETRY_MIN_DELTA_AGE_MS)
525 return 0;
526 return 1;
527}
528
529ckey_status ckey_telemetry_take_batch(const char *utc_day, char *out_json, size_t cap, size_t *out_len,
530 uint64_t *out_batch_id) {
531 if (!utc_day || !out_len || !out_batch_id || (!out_json && cap != 0)) return CKEY_INVALID_ARGUMENT;
532 if (!valid_day(utc_day)) return CKEY_INVALID_ARGUMENT;
533 std::lock_guard<std::mutex> lock(g_mutex);
534 State &s = g_state;
535 *out_len = 0;
536 *out_batch_id = 0;
537 if (s.mode != CKEY_TELEMETRY_ON || s.inflight_active || is_empty(s.delta)) {
538 if (cap) out_json[0] = '\0';
539 return CKEY_OK;
540 }
541 ckey_telemetry_counters remaining = s.delta;
542 ckey_telemetry_counters part = take_part(remaining);
543 Writer w{out_json, cap};
544 w.str("{\"schema\":");
545 w.num(CKEY_TELEMETRY_SCHEMA);
546 w.str(",\"sdk\":\"");
547 w.str(s.sdk);
548 w.str("\",\"sdk_version\":\"");
549 w.str(s.sdk_version);
550 w.str("\",\"platform\":\"");
551 w.str(s.platform);
552 w.str("\",\"day\":\"");
553 w.str(utc_day);
554 w.str("\",");
555 write_counters(w, part);
556 w.ch('}');
557 *out_len = w.len;
558 if (!w.finish()) return CKEY_INSUFFICIENT_BUFFER; // nothing moved
559 s.delta = remaining;
560 s.inflight = part;
561 s.inflight_active = true;
562 s.inflight_id = s.next_id++;
563 if (s.next_id == 0) s.next_id = 1;
564 if (is_empty(s.delta)) clear_delta(s);
565 else s.delta_ready = true; // a clamped remainder is not subject to the minimum age
566 *out_batch_id = s.inflight_id;
567 return CKEY_OK;
568}
569
570ckey_status ckey_telemetry_batch_result(uint64_t batch_id, int http_status, uint64_t now_ms) {
571 if (http_status < 0 || http_status > 999) return CKEY_INVALID_ARGUMENT;
572 std::lock_guard<std::mutex> lock(g_mutex);
573 State &s = g_state;
574 if (batch_id == 0 || !s.inflight_active || batch_id != s.inflight_id) return CKEY_INVALID_ARGUMENT;
575 bool keep = http_status == 0 || http_status == 429 || http_status < 200 || (http_status >= 300 && http_status < 400) ||
576 http_status >= 500;
577 if (keep && s.mode == CKEY_TELEMETRY_ON) {
578 merge(s.delta, s.inflight);
579 s.delta_ready = true; // already batched once; the hourly cadence still applies
580 }
581 drop_inflight(s);
582 s.has_last_attempt = true;
583 s.last_attempt = now_ms;
584 return CKEY_OK;
585}
586
587ckey_status ckey_telemetry_export_state(char *out, size_t cap, size_t *len) {
588 if (!len || (!out && cap != 0)) return CKEY_INVALID_ARGUMENT;
589 std::lock_guard<std::mutex> lock(g_mutex);
590 const State &s = g_state;
591 Writer w{out, cap};
592 w.str("{\"state\":1,\"totals\":{");
593 write_counters(w, s.totals);
594 w.str("},\"unsent\":{");
595 write_counters(w, s.delta);
596 w.str("}}");
597 *len = w.len;
598 return w.finish() ? CKEY_OK : CKEY_INSUFFICIENT_BUFFER;
599}
600
601ckey_status ckey_telemetry_import_state(const char *json) {
602 if (!json) return CKEY_INVALID_ARGUMENT;
603 size_t n = strnlen(json, kMaxImportBytes + 1);
604 if (n > kMaxImportBytes) return CKEY_INVALID_ARGUMENT;
605 Parser p{json, n};
606 ckey_telemetry_counters totals{}, unsent{};
607 uint64_t version = 0;
608 if (!p.lit("{") || !p.key_u64("state", version) || version != 1 || !p.lit(",") || !p.key("totals") ||
609 !parse_counters(p, totals) || !p.lit(",") || !p.key("unsent") || !parse_counters(p, unsent) || !p.lit("}"))
610 return CKEY_INVALID_ARGUMENT;
611 p.ws();
612 if (p.i != n) return CKEY_INVALID_ARGUMENT;
613 std::lock_guard<std::mutex> lock(g_mutex);
614 State &s = g_state;
615 if (s.mode == CKEY_TELEMETRY_OFF) return CKEY_INVALID_ARGUMENT;
616 merge(s.totals, totals);
617 if (s.mode == CKEY_TELEMETRY_ON && !is_empty(unsent)) {
618 merge(s.delta, unsent);
619 s.delta_ready = true;
620 }
621 return CKEY_OK;
622}
623
624void ckey_telemetry_reset_for_testing(void) {
625 std::lock_guard<std::mutex> lock(g_mutex);
626 uint64_t next_id = g_state.next_id; // keep ids unique across resets
627 g_state = State{};
628 g_state.next_id = next_id;
629}
630
631} // extern "C"