Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
51 changes: 40 additions & 11 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand All @@ -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
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/vendor/json/include>
$<INSTALL_INTERFACE:include>)
endif()
if (COUNTLY_BUILD_TESTS)
message("Compiling definitions for tests")
target_compile_definitions(countly PRIVATE COUNTLY_BUILD_TESTS)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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()
9 changes: 6 additions & 3 deletions include/countly.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -464,17 +464,20 @@ 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();
bool began_session = false;
std::atomic<bool> is_being_disposed{false};
std::atomic<bool> 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<std::thread> thread;
Expand Down
11 changes: 11 additions & 0 deletions include/countly/constants.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <cassert>
#include <chrono>
#include <climits>
Expand Down
3 changes: 3 additions & 0 deletions include/countly/event.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 6 additions & 6 deletions src/countly.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<std::chrono::milliseconds>(now.time_since_epoch());
const auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(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
Expand Down Expand Up @@ -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<std::mutex> 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]");
Expand Down
5 changes: 3 additions & 2 deletions src/event.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::chrono::milliseconds>(timestamp.time_since_epoch()).count();

std::time_t time = std::chrono::system_clock::to_time_t(timestamp);
Expand All @@ -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<std::chrono::seconds>(now - timestamp).count();
auto now = std::chrono::steady_clock::now();
object["dur"] = std::chrono::duration_cast<std::chrono::seconds>(now - timer_start).count();
timer_running = false;
}
}
Expand Down
11 changes: 6 additions & 5 deletions src/views_module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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<std::chrono::seconds>(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<std::chrono::seconds>(now - v->startTime);
duration = dur.count();
}

Expand Down Expand Up @@ -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::seconds>(std::chrono::system_clock::now().time_since_epoch());
v->startTime = std::chrono::steady_clock::now();

std::shared_ptr<ViewModuleImpl::ViewInfo> ptr(v);

Expand Down
Loading