diff --git a/CHANGELOG.md b/CHANGELOG.md index 106cd94..12c4481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to the Tombstack Native SDK (the `tombstone_*` C ABI and the `tombstone` library name are stable — Tombstack is the product name). +## [0.9.2] - 2026-08-25 + +### Fixed — heartbeat ceiling lowered to 240s (behaviour change; no API/ABI change) + +- **`options.heartbeat_interval_s` is now clamped to `[15, 240]`, was `[15, 600]`.** The ceiling is a + BILLING constant, not a preference. The server merges consecutive beats into one live session only + while the gap stays under its 300s session window, so a 600s cadence shattered a continuously-online + player into isolated instants: concurrent sessions stopped overlapping, monthly **peak CCU** read low, + and the studio was invoiced on the low figure. An integrator raising the interval to cut telemetry + cost — explicitly permitted by the header's own comment — silently reduced their own bill. + + This is bit-for-bit the defect the Unity SDK shipped in 0.19.3. The Tombstack repo's + `tests/heartbeat-cadence.test.ts` was written to stop it recurring, but read the C# source **only**, + so this SDK kept 600 with a green suite. That guard now parses the ceiling out of this repo's public + header as well, and asserts all three SDKs (Unity, Godot, native) agree. + + A caller that passed a value above 240 gets 240 instead. Nothing else about the cadence changes; the + default (60s) and the floor (15s) are untouched. + ## [0.9.1] - 2026-07-20 ### Hardening (production audit; no API/ABI change) diff --git a/CMakeLists.txt b/CMakeLists.txt index f9fb155..407ae18 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,8 @@ cmake_minimum_required(VERSION 3.16) -project(tombstone VERSION 0.9.1 LANGUAGES C CXX) +# NOTE: this version is DUPLICATED as a literal in src/tombstone_api.cpp (`sdk_version`), which is what +# tombstone_version() returns. Bump both together — 0.9.1's changelog records that the reported version +# was once stuck at 0.8.0 for exactly this reason. Deriving it from PROJECT_VERSION is the real fix. +project(tombstone VERSION 0.9.2 LANGUAGES C CXX) option(TOMBSTONE_BUILD_STATIC "Also build a static tombstone library" OFF) option(TOMBSTONE_BUILD_EXAMPLES "Build the example programs" ON) diff --git a/include/tombstone/tombstone.h b/include/tombstone/tombstone.h index 3dec3b6..a5f3e0d 100644 --- a/include/tombstone/tombstone.h +++ b/include/tombstone/tombstone.h @@ -104,7 +104,11 @@ typedef struct tombstone_options { * "staging", "dev"; clamped to 64). NULL/empty = unset (the server * defaults to "production"). tombstone_set_environment() overrides this. */ const char *environment; - /** Seconds between session heartbeats; clamped to [15, 600]. 0 = default (60). */ + /** Seconds between session heartbeats; clamped to [15, 240]. 0 = default (60). + * The ceiling is a BILLING constant, not a preference: the server merges + * consecutive beats into one live session only while the gap stays under its + * 300s window, so a slower cadence shatters a continuous session into isolated + * instants and the studio's monthly peak-CCU invoice comes out low. */ int heartbeat_interval_s; /** Nonzero (default) = emit periodic session heartbeats. */ int enable_heartbeats; diff --git a/src/client.cpp b/src/client.cpp index 3a9f34d..c4fc293 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -31,7 +31,18 @@ constexpr const char *metrics_batch_path = "/api/v1/ingest/metrics:batch"; constexpr const char *pull_requests_path = "/api/v1/pull-requests"; constexpr int min_heartbeat_interval_s = 15; -constexpr int max_heartbeat_interval_s = 600; +// 240, NOT 600 — the server's session window is 300s and this is a BILLING constant. +// +// computePeakConcurrency merges consecutive beats into one live span only while the gap is <= that +// window. At 600s a player online for an hour produces six isolated instants instead of one span, so +// concurrent players stop overlapping, the monthly PEAK CCU comes out low, and the studio is invoiced +// on the low figure. Unity shipped exactly this (0.19.3, MAX_HEARTBEAT_INTERVAL_SECONDS = 600f) and +// the Tombstack repo carries tests/heartbeat-cadence.test.ts to stop it recurring — but that guard read +// the C# source only, which is why this side kept 600. It now reads the C header's range as well. +// +// The 20% headroom is deliberate: at exactly the window each beat expires as the next arrives, so any +// scheduling jitter or upload latency opens a gap. +constexpr int max_heartbeat_interval_s = 240; constexpr int default_heartbeat_interval_s = 60; constexpr const char *identity_file_name = "identity"; diff --git a/src/tombstone_api.cpp b/src/tombstone_api.cpp index 305385a..c3d151a 100644 --- a/src/tombstone_api.cpp +++ b/src/tombstone_api.cpp @@ -12,7 +12,10 @@ namespace { -constexpr const char *sdk_version = "0.9.1"; +// Kept in lockstep with project(... VERSION ...) in CMakeLists.txt BY HAND, and that has failed TWICE: +// the changelog records this string pinned at "0.1.0" once and stuck at "0.8.0" once. Deriving it from +// PROJECT_VERSION is the real fix; until then, bump both or ship a third wrong version. +constexpr const char *sdk_version = "0.9.2"; // The process-wide client. Entry points snapshot the shared_ptr under the // mutex and call outside it, so a long flush() neither blocks other calls nor