From c9455adcc989883888fe7aca0314d0f8e2b01665 Mon Sep 17 00:00:00 2001 From: turtledreams Date: Thu, 20 Aug 2026 22:22:49 +0900 Subject: [PATCH] Issue Fixes --- CHANGELOG.md | 8 ++++++ CMakeLists.txt | 51 +++++++++++++++++++++++++++-------- include/countly.hpp | 9 ++++--- include/countly/constants.hpp | 11 ++++++++ include/countly/event.hpp | 3 +++ src/countly.cpp | 12 ++++----- src/event.cpp | 5 ++-- src/views_module.cpp | 11 ++++---- 8 files changed, 83 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b6e36f..a232cd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## Next release +- Static library builds on non-Windows platforms no longer require a custom HTTP client; the built-in libcurl client can now be used there as well. + +- Added the `COUNTLY_USE_SYSTEM_JSON` CMake option to compile the SDK against the application's own nlohmann/json (3.11 or newer) instead of the bundled copy, for applications that already use the library. The SDK headers now reject unsupported nlohmann/json versions at compile time with a clear error. + +- Fixed session, view and timed-event durations being distorted when the system clock changed while the application was running; elapsed time is now measured with a monotonic clock. +- Fixed `cmake --install` producing an unusable include layout: `countly.hpp` is now installed into the include root, all public headers (several were missing, including `countly_configuration.hpp`) are installed under `include/countly`, and the bundled nlohmann/json headers are installed alongside them, so the installed tree compiles on its own. + ## 26.8.0 - ! Minor breaking change ! The SDK now enforces internal limits on recorded data (key length, value size, segmentation entry count, breadcrumb count, stack trace lines per thread and line length) across events, views, crashes, and user properties. Defaults can be overridden by server-side SDK Behavior Settings, or during init via `setMaxKeyLength`, `setMaxValueSize`, `setMaxSegmentationValues`, `setMaxBreadcrumbCount`, `setMaxStackTraceLinesPerThread` and `setMaxStackTraceLineLength`. - ! Minor breaking change ! Updated the bundled nlohmann/json from v3.7.0 to v3.12.0. JSON types are part of the public API, so applications must be recompiled against the new headers; binaries built against the old headers will not link against a library built with the new ones. diff --git a/CMakeLists.txt b/CMakeLists.txt index b90cc77..162efa2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ option(COUNTLY_USE_CUSTOM_HTTP "Use a custom HTTP library" OFF) option(COUNTLY_USE_CUSTOM_SHA256 "Use a custom SHA 256 library" OFF) option(COUNTLY_USE_SQLITE "Use SQLite" OFF) option(COUNTLY_USE_SYSTEM_SQLITE "Link the system SQLite instead of compiling the vendored amalgamation" OFF) +option(COUNTLY_USE_SYSTEM_JSON "Compile against the system nlohmann_json instead of the vendored copy" OFF) option(COUNTLY_BUILD_TESTS "Build test programs" OFF) option(COUNTLY_BUILD_SAMPLE "Build Sample programs" OFF) @@ -23,25 +24,30 @@ message("Use a custom HTTP library:" ${COUNTLY_USE_CUSTOM_HTTP}) message("Use a custom SHA 256 library:" ${COUNTLY_USE_CUSTOM_SHA256}) message("Use SQLite:" ${COUNTLY_USE_SQLITE}) message("Use system SQLite:" ${COUNTLY_USE_SYSTEM_SQLITE}) +message("Use system nlohmann_json:" ${COUNTLY_USE_SYSTEM_JSON}) message("Build test programs:" ${COUNTLY_BUILD_TESTS}) message("Build Sample programs:" ${COUNTLY_BUILD_SAMPLE}) -if (NOT WIN32 AND NOT BUILD_SHARED_LIBS AND NOT COUNTLY_USE_CUSTOM_HTTP) - message(FATAL_ERROR "You must provide a custom HTTP function when compiling statically.") -endif() - -# Compile countly public headers +# Compile countly public headers: everything countly.hpp includes, so an +# installed copy of the SDK is self-contained. countly.hpp itself is installed +# separately into the include root (see the install rules at the bottom). set(COUNTLY_PUBLIC_HEADERS - ${CMAKE_CURRENT_SOURCE_DIR}/include/countly.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/countly/configuration_module.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/countly/configuration_provider.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/countly/constants.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/countly/countly_configuration.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/countly/crash_module.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/countly/event.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/include/countly/logger_module.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/include/countly/crash_module.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/countly/remote_config_store.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/countly/request_builder.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/countly/request_module.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/countly/storage_module_base.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/countly/views_module.hpp) add_library(countly ${COUNTLY_PUBLIC_HEADERS} + ${CMAKE_CURRENT_SOURCE_DIR}/include/countly.hpp ${CMAKE_CURRENT_SOURCE_DIR}/src/countly.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/views_module.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/logger_module.cpp @@ -66,7 +72,21 @@ set_target_properties(countly PROPERTIES find_package(Threads) target_link_libraries(countly Threads::Threads) -target_include_directories(countly PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/vendor/json/include) +# nlohmann/json is part of the public API, so its headers are a usage +# requirement of the countly target. By default the vendored copy is used and +# later installed next to the SDK headers, so the installed tree is +# self-contained. An application that already uses nlohmann/json should set +# COUNTLY_USE_SYSTEM_JSON to compile the SDK against that same copy instead, +# so only one version of the library exists in the build (the SDK headers +# reject unsupported versions at compile time). +if(COUNTLY_USE_SYSTEM_JSON) + find_package(nlohmann_json 3.11 REQUIRED) + target_link_libraries(countly nlohmann_json::nlohmann_json) +else() + target_include_directories(countly PUBLIC + $ + $) +endif() if (COUNTLY_BUILD_TESTS) message("Compiling definitions for tests") target_compile_definitions(countly PRIVATE COUNTLY_BUILD_TESTS) @@ -154,7 +174,7 @@ if(COUNTLY_BUILD_TESTS) target_compile_definitions(countly-tests PRIVATE COUNTLY_USE_CUSTOM_SHA256) endif() target_include_directories(countly-tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/doctest/doctest) - target_include_directories(countly-tests PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/vendor/json/include) + # nlohmann/json comes in as a usage requirement of the countly target. target_link_libraries(countly-tests countly) set_target_properties(countly-tests PROPERTIES CXX_STANDARD 14 @@ -177,7 +197,7 @@ if(COUNTLY_USE_SQLITE) endif() endif() target_include_directories(countly-sample PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/doctest/doctest) - target_include_directories(countly-sample PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/vendor/json/include) + # nlohmann/json comes in as a usage requirement of the countly target. target_link_libraries(countly-sample countly) set_target_properties(countly-sample PROPERTIES CXX_STANDARD 14 @@ -190,3 +210,12 @@ install(TARGETS countly LIBRARY DESTINATION lib RUNTIME DESTINATION bin PUBLIC_HEADER DESTINATION include/countly) +# countly.hpp lives in the include root, so `#include "countly.hpp"` works +# against the installed tree just like against the source tree. +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/include/countly.hpp DESTINATION include) +# The SDK headers include "nlohmann/json.hpp", so ship the vendored copy with +# them; without it an installed tree does not compile on its own. Skipped when +# building against the system copy, which the application provides itself. +if(NOT COUNTLY_USE_SYSTEM_JSON) + install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/vendor/json/include/nlohmann DESTINATION include) +endif() diff --git a/include/countly.hpp b/include/countly.hpp index 335e077..1ab79e3 100644 --- a/include/countly.hpp +++ b/include/countly.hpp @@ -464,9 +464,9 @@ class Countly : public cly::CountlyDelegates { void _changeDeviceIdWithoutMerge(const std::string &value); - std::chrono::system_clock::duration getSessionDuration(std::chrono::system_clock::time_point now); + std::chrono::steady_clock::duration getSessionDuration(std::chrono::steady_clock::time_point now); - std::chrono::system_clock::duration getSessionDuration(); + std::chrono::steady_clock::duration getSessionDuration(); void updateLoop(); void packEvents(); @@ -474,7 +474,10 @@ class Countly : public cly::CountlyDelegates { std::atomic is_being_disposed{false}; std::atomic is_sdk_initialized{false}; - std::chrono::system_clock::time_point last_sent_session_request; + // Monotonic, not system_clock: session duration is elapsed time, and a + // system clock change mid-session must not shrink it or turn it negative + // (issue #100). Wall-clock timestamps in requests still use system_clock. + std::chrono::steady_clock::time_point last_sent_session_request; nlohmann::json session_params; std::unique_ptr thread; diff --git a/include/countly/constants.hpp b/include/countly/constants.hpp index dccde59..3163203 100644 --- a/include/countly/constants.hpp +++ b/include/countly/constants.hpp @@ -2,6 +2,17 @@ #define COUNTLY_CONSTANTS_HPP_ #include "nlohmann/json.hpp" + +// nlohmann/json is part of the SDK's public API, so the copy these headers are +// compiled against must be the one the library was built with. If another copy +// of the library wins the include-path race (both use the same include guard, +// so only the first one found is ever seen), fail here with a clear message +// instead of failing at link time. Versions from 3.11 on carry an ABI-tagged +// inline namespace, which is what makes a mismatch a linker error rather than +// silent memory corruption. To build against your application's own copy, use +// the COUNTLY_USE_SYSTEM_JSON CMake option. +static_assert(NLOHMANN_JSON_VERSION_MAJOR == 3 && NLOHMANN_JSON_VERSION_MINOR >= 11, "Countly SDK requires nlohmann/json 3.11 or newer (3.x). An incompatible copy of nlohmann/json was found first on the include path."); + #include #include #include diff --git a/include/countly/event.hpp b/include/countly/event.hpp index b09f60c..5a5e58c 100644 --- a/include/countly/event.hpp +++ b/include/countly/event.hpp @@ -42,6 +42,9 @@ class Event { nlohmann::json object; bool timer_running; std::chrono::system_clock::time_point timestamp; + // Durations are measured on the monotonic clock so a system clock change + // while a timed event runs cannot produce a wrong or negative duration. + std::chrono::steady_clock::time_point timer_start; }; } // namespace cly #endif diff --git a/src/countly.cpp b/src/countly.cpp index 1ed69b8..45c0a5a 100644 --- a/src/countly.cpp +++ b/src/countly.cpp @@ -1389,7 +1389,7 @@ bool Countly::beginSession() { requestModule->addRequestToQueue(data); session_params.erase("user_details"); - last_sent_session_request = Countly::getTimestamp(); + last_sent_session_request = std::chrono::steady_clock::now(); began_session = true; // snapshot guarded state before releasing the lock bool shouldUpdateRemoteConfig = remote_config_enabled; @@ -1568,8 +1568,8 @@ bool Countly::endSession() { log(LogLevel::ERROR, "[Countly] endSession, Session tracking is disabled in server configuration, can not end session."); return false; } - const std::chrono::system_clock::time_point now = Countly::getTimestamp(); - const auto timestamp = std::chrono::duration_cast(now.time_since_epoch()); + const auto timestamp = std::chrono::duration_cast(Countly::getTimestamp().time_since_epoch()); + const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); // lock_guard so the mutex is released on scope exit, including the early // returns below and any exception (e.g. a json type_error from a session_params @@ -1933,13 +1933,13 @@ std::string Countly::calculateChecksum(const std::string &salt, const std::strin #endif } -std::chrono::system_clock::duration Countly::getSessionDuration(std::chrono::system_clock::time_point now) { +std::chrono::steady_clock::duration Countly::getSessionDuration(std::chrono::steady_clock::time_point now) { std::lock_guard lk(*mutex); - std::chrono::system_clock::duration duration = now - last_sent_session_request; + std::chrono::steady_clock::duration duration = now - last_sent_session_request; return duration; } -std::chrono::system_clock::duration Countly::getSessionDuration() { return Countly::getSessionDuration(Countly::getTimestamp()); } +std::chrono::steady_clock::duration Countly::getSessionDuration() { return Countly::getSessionDuration(std::chrono::steady_clock::now()); } void Countly::updateLoop() { log(LogLevel::DEBUG, "[Countly][updateLoop]"); diff --git a/src/event.cpp b/src/event.cpp index 5f87be0..c9e6c6e 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -28,6 +28,7 @@ Event::Event(const std::string &key, size_t count, double sum, double duration) void Event::setTimestamp() { timestamp = std::chrono::system_clock::now(); + timer_start = std::chrono::steady_clock::now(); object["timestamp"] = std::chrono::duration_cast(timestamp.time_since_epoch()).count(); std::time_t time = std::chrono::system_clock::to_time_t(timestamp); @@ -45,8 +46,8 @@ void Event::startTimer() { void Event::stopTimer() { if (timer_running) { - auto now = std::chrono::system_clock::now(); - object["dur"] = std::chrono::duration_cast(now - timestamp).count(); + auto now = std::chrono::steady_clock::now(); + object["dur"] = std::chrono::duration_cast(now - timer_start).count(); timer_running = false; } } diff --git a/src/views_module.cpp b/src/views_module.cpp index 9302bc9..bcfcfba 100644 --- a/src/views_module.cpp +++ b/src/views_module.cpp @@ -13,7 +13,9 @@ class ViewsModule::ViewModuleImpl { public: std::string name; std::string viewId; - std::chrono::seconds startTime; + // Monotonic, not system_clock: the view duration is elapsed time, and a + // system clock change while the view is open must not distort it (issue #100). + std::chrono::steady_clock::time_point startTime; }; private: @@ -67,9 +69,8 @@ class ViewsModule::ViewModuleImpl { } } else { - const std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); - const auto timestamp = std::chrono::duration_cast(now.time_since_epoch()); - std::chrono::seconds dur = timestamp - v->startTime; + const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + const auto dur = std::chrono::duration_cast(now - v->startTime); duration = dur.count(); } @@ -107,7 +108,7 @@ class ViewsModule::ViewModuleImpl { ViewModuleImpl::ViewInfo *v = new ViewModuleImpl::ViewInfo(); v->name = limitedName; v->viewId = cly::utils::generateEventID(); - v->startTime = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()); + v->startTime = std::chrono::steady_clock::now(); std::shared_ptr ptr(v);