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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
6 changes: 6 additions & 0 deletions src/event.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "countly/event.hpp"
#include <ctime>

namespace cly {
Event::Event(const std::string &key, size_t count) : object({}), timer_running(false) {
Expand Down Expand Up @@ -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<std::chrono::milliseconds>(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() {
Expand Down
22 changes: 21 additions & 1 deletion src/request_builder.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "countly/request_builder.hpp"
#include <chrono>
#include <ctime>
#include <iomanip>
#include <iostream>
#include <sstream>
Expand All @@ -26,7 +27,26 @@ std::string RequestBuilder::buildRequest(const std::map<std::string, std::string
const std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
const auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch());

std::map<std::string, std::string> 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<std::string, std::string> 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);
Expand Down
41 changes: 41 additions & 0 deletions tests/event.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "countly/event.hpp"
#include "doctest.h"
#include <ctime>
#include <iostream>
#include <string>
using namespace cly;
Expand All @@ -8,6 +9,14 @@ void valideEventParams(nlohmann::json eventJson, const std::string key, int coun
CHECK(eventJson["key"].get<std::string>() == key);
CHECK(eventJson["count"].get<int>() == count);
CHECK(std::to_string(eventJson["timestamp"].get<long long>()).size() == 13);
CHECK(eventJson.contains("dow"));
CHECK(eventJson.contains("hour"));
int dow = eventJson["dow"].get<int>();
int hour = eventJson["hour"].get<int>();
CHECK(dow >= 0);
CHECK(dow <= 6);
CHECK(hour >= 0);
CHECK(hour <= 23);
}

TEST_CASE("events are serialized correctly") {
Expand Down Expand Up @@ -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<int>() == local_tm.tm_wday);
CHECK(e["hour"].get<int>() == 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>();
int hour = after["hour"].get<int>();
CHECK(dow >= 0);
CHECK(dow <= 6);
CHECK(hour >= 0);
CHECK(hour <= 23);
}
}
109 changes: 107 additions & 2 deletions tests/request.cpp
Original file line number Diff line number Diff line change
@@ -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 <ctime>
#include <iostream>
#include <string>
#include <vector>
Expand Down Expand Up @@ -42,7 +44,10 @@ void ValidateRequestSizeOnReachingThresholdLimit(std::shared_ptr<StorageModuleBa

std::shared_ptr<DataEntry> frontRequest = storageModule->RQPeekFront();
CHECK(frontRequest->getId() == 2);
CHECK(frontRequest->getData().substr(0, 33) == "app_key=&device_id=&param2=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") {
Expand Down Expand Up @@ -83,4 +88,104 @@ TEST_CASE("Test Request Module with SQLite Storage") {

SUBCASE("Validate request queue threshold") { ValidateRequestSizeOnReachingThresholdLimit(storageModule, requestModule); }
}
#endif
#endif

// Helper to parse a serialized request string into a key-value map
static std::map<std::string, std::string> parseRequest(const std::string &data) {
std::map<std::string, std::string> 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<cly::LoggerModule> logger = std::make_shared<cly::LoggerModule>();
shared_ptr<cly::CountlyConfiguration> configuration = std::make_shared<CountlyConfiguration>("test_app_key", "test_device_id");
std::shared_ptr<RequestBuilder> requestBuilder = std::make_shared<RequestBuilder>(configuration, logger);

SUBCASE("buildRequest includes dow, hour, and tz fields") {
std::map<std::string, std::string> 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<std::string, std::string> 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<std::string, std::string> 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<std::string, std::string> 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<std::string, std::string> 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<std::string, std::string> 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);
}
}
Loading