From e85d02be14b7ce612ce27e383365624f908af336 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 31 Mar 2026 16:55:01 +0300 Subject: [PATCH 1/3] feat: hour dow tz to events and requests --- src/event.cpp | 6 ++++++ src/request_builder.cpp | 22 +++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/event.cpp b/src/event.cpp index 41caffb..cf81bd0 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -1,4 +1,5 @@ #include "countly/event.hpp" +#include namespace cly { Event::Event(const std::string &key, size_t count) : object({}), timer_running(false) { @@ -26,6 +27,11 @@ Event::Event(const std::string &key, size_t count, double sum, double duration) void Event::setTimestamp() { timestamp = std::chrono::system_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); + std::tm local_tm = *std::localtime(&time); + object["dow"] = local_tm.tm_wday; + object["hour"] = local_tm.tm_hour; } void Event::startTimer() { diff --git a/src/request_builder.cpp b/src/request_builder.cpp index d685f8f..b391a5d 100644 --- a/src/request_builder.cpp +++ b/src/request_builder.cpp @@ -1,5 +1,6 @@ #include "countly/request_builder.hpp" #include +#include #include #include #include @@ -26,7 +27,26 @@ std::string RequestBuilder::buildRequest(const std::map(now.time_since_epoch()); - std::map request = {{"app_key", _configuration->appKey}, {"device_id", _configuration->deviceId}, {"timestamp", std::to_string(timestamp.count())}}; + std::time_t time = std::chrono::system_clock::to_time_t(now); + std::tm local_tm = *std::localtime(&time); + std::tm gm_tm = *std::gmtime(&time); + + int tz_offset_minutes = (local_tm.tm_hour - gm_tm.tm_hour) * 60 + (local_tm.tm_min - gm_tm.tm_min); + // Adjust for day boundary crossings + int day_diff = local_tm.tm_mday - gm_tm.tm_mday; + if (day_diff > 1) { + day_diff = -1; // end of month wrap + } else if (day_diff < -1) { + day_diff = 1; // end of month wrap + } + tz_offset_minutes += day_diff * 24 * 60; + + std::map request = {{"app_key", _configuration->appKey}, + {"device_id", _configuration->deviceId}, + {"timestamp", std::to_string(timestamp.count())}, + {"dow", std::to_string(local_tm.tm_wday)}, + {"hour", std::to_string(local_tm.tm_hour)}, + {"tz", std::to_string(tz_offset_minutes)}}; request.insert(data.begin(), data.end()); return serializeData(request); From 0e5951b68053957cac96f29e1d3f52957f6cdc25 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 31 Mar 2026 16:55:16 +0300 Subject: [PATCH 2/3] feat: tests --- tests/event.cpp | 41 +++++++++++++++++ tests/request.cpp | 109 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 148 insertions(+), 2 deletions(-) diff --git a/tests/event.cpp b/tests/event.cpp index 9213013..1faa3a6 100644 --- a/tests/event.cpp +++ b/tests/event.cpp @@ -1,5 +1,6 @@ #include "countly/event.hpp" #include "doctest.h" +#include #include #include using namespace cly; @@ -8,6 +9,14 @@ void valideEventParams(nlohmann::json eventJson, const std::string key, int coun CHECK(eventJson["key"].get() == key); CHECK(eventJson["count"].get() == count); CHECK(std::to_string(eventJson["timestamp"].get()).size() == 13); + CHECK(eventJson.contains("dow")); + CHECK(eventJson.contains("hour")); + int dow = eventJson["dow"].get(); + int hour = eventJson["hour"].get(); + CHECK(dow >= 0); + CHECK(dow <= 6); + CHECK(hour >= 0); + CHECK(hour <= 23); } TEST_CASE("events are serialized correctly") { @@ -98,3 +107,35 @@ TEST_CASE("events are serialized correctly") { } } } + +TEST_CASE("events contain correct dow and hour") { + SUBCASE("dow and hour match local time") { + cly::Event event("test_time", 1); + nlohmann::json e = nlohmann::json::parse(event.serialize()); + + std::time_t now = std::time(nullptr); + std::tm local_tm = *std::localtime(&now); + + CHECK(e["dow"].get() == local_tm.tm_wday); + CHECK(e["hour"].get() == local_tm.tm_hour); + } + + SUBCASE("dow and hour are updated on startTimer") { + cly::Event event("timed_event", 1); + nlohmann::json before = nlohmann::json::parse(event.serialize()); + + CHECK(before.contains("dow")); + CHECK(before.contains("hour")); + + event.startTimer(); + nlohmann::json after = nlohmann::json::parse(event.serialize()); + + // dow and hour should still be valid after startTimer resets timestamp + int dow = after["dow"].get(); + int hour = after["hour"].get(); + CHECK(dow >= 0); + CHECK(dow <= 6); + CHECK(hour >= 0); + CHECK(hour <= 23); + } +} diff --git a/tests/request.cpp b/tests/request.cpp index d88c745..d6072e2 100644 --- a/tests/request.cpp +++ b/tests/request.cpp @@ -1,9 +1,11 @@ #include "countly/storage_module_base.hpp" #include "countly/storage_module_db.hpp" #include "countly/storage_module_memory.hpp" +#include "countly/request_builder.hpp" #include "test_utils.hpp" #include "doctest.h" +#include #include #include #include @@ -42,7 +44,10 @@ void ValidateRequestSizeOnReachingThresholdLimit(std::shared_ptr frontRequest = storageModule->RQPeekFront(); CHECK(frontRequest->getId() == 2); - CHECK(frontRequest->getData().substr(0, 33) == "app_key=&device_id=¶m2=value2"); + std::string requestData = frontRequest->getData(); + CHECK(requestData.find("app_key=") != std::string::npos); + CHECK(requestData.find("device_id=") != std::string::npos); + CHECK(requestData.find("param2=value2") != std::string::npos); } TEST_CASE("Test Request Module with Memory Storage") { @@ -83,4 +88,104 @@ TEST_CASE("Test Request Module with SQLite Storage") { SUBCASE("Validate request queue threshold") { ValidateRequestSizeOnReachingThresholdLimit(storageModule, requestModule); } } -#endif \ No newline at end of file +#endif + +// Helper to parse a serialized request string into a key-value map +static std::map parseRequest(const std::string &data) { + std::map result; + std::string::size_type start = 0; + while (start < data.size()) { + auto ampersand = data.find('&', start); + if (ampersand == std::string::npos) { + ampersand = data.size(); + } + auto eq = data.find('=', start); + if (eq != std::string::npos && eq < ampersand) { + result[data.substr(start, eq - start)] = data.substr(eq + 1, ampersand - eq - 1); + } + start = ampersand + 1; + } + return result; +} + +TEST_CASE("Requests contain dow, hour, and tz") { + shared_ptr logger = std::make_shared(); + shared_ptr configuration = std::make_shared("test_app_key", "test_device_id"); + std::shared_ptr requestBuilder = std::make_shared(configuration, logger); + + SUBCASE("buildRequest includes dow, hour, and tz fields") { + std::map data = {{"test_key", "test_value"}}; + std::string request = requestBuilder->buildRequest(data); + + auto parsed = parseRequest(request); + + CHECK(parsed.find("dow") != parsed.end()); + CHECK(parsed.find("hour") != parsed.end()); + CHECK(parsed.find("tz") != parsed.end()); + CHECK(parsed.find("timestamp") != parsed.end()); + } + + SUBCASE("dow is between 0 and 6") { + std::map data; + std::string request = requestBuilder->buildRequest(data); + auto parsed = parseRequest(request); + + int dow = std::stoi(parsed["dow"]); + CHECK(dow >= 0); + CHECK(dow <= 6); + } + + SUBCASE("hour is between 0 and 23") { + std::map data; + std::string request = requestBuilder->buildRequest(data); + auto parsed = parseRequest(request); + + int hour = std::stoi(parsed["hour"]); + CHECK(hour >= 0); + CHECK(hour <= 23); + } + + SUBCASE("tz is a valid timezone offset in minutes") { + std::map data; + std::string request = requestBuilder->buildRequest(data); + auto parsed = parseRequest(request); + + int tz = std::stoi(parsed["tz"]); + // Valid timezone offsets range from UTC-12 (-720) to UTC+14 (+840) + CHECK(tz >= -720); + CHECK(tz <= 840); + } + + SUBCASE("dow and hour match current local time") { + std::map data; + std::string request = requestBuilder->buildRequest(data); + auto parsed = parseRequest(request); + + std::time_t now = std::time(nullptr); + std::tm local_tm = *std::localtime(&now); + + CHECK(std::stoi(parsed["dow"]) == local_tm.tm_wday); + CHECK(std::stoi(parsed["hour"]) == local_tm.tm_hour); + } + + SUBCASE("tz matches current timezone offset") { + std::map data; + std::string request = requestBuilder->buildRequest(data); + auto parsed = parseRequest(request); + + std::time_t now = std::time(nullptr); + std::tm local_tm = *std::localtime(&now); + std::tm gm_tm = *std::gmtime(&now); + + int expected_tz = (local_tm.tm_hour - gm_tm.tm_hour) * 60 + (local_tm.tm_min - gm_tm.tm_min); + int day_diff = local_tm.tm_mday - gm_tm.tm_mday; + if (day_diff > 1) { + day_diff = -1; + } else if (day_diff < -1) { + day_diff = 1; + } + expected_tz += day_diff * 24 * 60; + + CHECK(std::stoi(parsed["tz"]) == expected_tz); + } +} \ No newline at end of file From 8b9cd57772e12336da0541a878dc1cec134d7ae0 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 31 Mar 2026 16:56:08 +0300 Subject: [PATCH 3/3] feat: changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfa2622..39ccb01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## XX.XX.XX - Fixed OpenSSL discovery in CMakeLists.txt to dynamically resolve the Homebrew prefix, supporting both Apple Silicon and Intel Macs. +- Added `dow` (day of week) and `hour` fields to every event. +- Added `dow`, `hour`, and `tz` (timezone offset in minutes) fields to every request. ## 23.2.4 - Mitigated an issue where cached events were not queued when a user property was recorded.