diff --git a/CHANGELOG.md b/CHANGELOG.md index 23a645c..6c1bf66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 23.2.4 +- Mitigated an issue where cached events were not queued when a user property was recorded. + ## 23.2.3 - Mitigated an issue where the new device ID was used when ending a session if device ID was changed without merging. diff --git a/include/countly.hpp b/include/countly.hpp index fa985e3..6065b9d 100644 --- a/include/countly.hpp +++ b/include/countly.hpp @@ -58,6 +58,8 @@ class Countly : public cly::CountlyDelegates { void enableManualSessionControl(); + void disableAutoEventsOnUserProperties(); + void setHTTPClient(HTTPClientFunction fun); void setMetrics(const std::string &os, const std::string &os_version, const std::string &device, const std::string &resolution, const std::string &carrier, const std::string &app_version); diff --git a/include/countly/constants.hpp b/include/countly/constants.hpp index 5ea5367..52b436c 100644 --- a/include/countly/constants.hpp +++ b/include/countly/constants.hpp @@ -13,7 +13,7 @@ #include #define COUNTLY_SDK_NAME "cpp-native-unknown" -#define COUNTLY_SDK_VERSION "23.2.3" +#define COUNTLY_SDK_VERSION "23.2.4" #define COUNTLY_POST_THRESHOLD 2000 #define COUNTLY_KEEPALIVE_INTERVAL 3000 #define COUNTLY_MAX_EVENTS_DEFAULT 200 diff --git a/include/countly/countly_configuration.hpp b/include/countly/countly_configuration.hpp index c98d500..6540338 100644 --- a/include/countly/countly_configuration.hpp +++ b/include/countly/countly_configuration.hpp @@ -70,6 +70,8 @@ struct CountlyConfiguration { bool manualSessionControl = false; + bool autoEventsOnUserProperties = true; + HTTPClientFunction http_client_function = nullptr; nlohmann::json metrics; diff --git a/src/countly.cpp b/src/countly.cpp index 42552d6..bf01fef 100644 --- a/src/countly.cpp +++ b/src/countly.cpp @@ -142,6 +142,20 @@ void Countly::enableManualSessionControl() { mutex->unlock(); } +/** + * Disable automatic events on user properties changes. + */ +void Countly::disableAutoEventsOnUserProperties() { + if (is_sdk_initialized) { + log(LogLevel::WARNING, "[Countly][disableAutoEventsOnUserProperties] You can not disable automatic events on user properties after SDK initialization."); + return; + } + + mutex->lock(); + configuration->autoEventsOnUserProperties = false; + mutex->unlock(); +} + void Countly::setMetrics(const std::string &os, const std::string &os_version, const std::string &device, const std::string &resolution, const std::string &carrier, const std::string &app_version) { if (is_sdk_initialized) { log(LogLevel::WARNING, "[Countly][setMetrics] You can not set metrics after SDK initialization."); @@ -182,6 +196,12 @@ void Countly::setUserDetails(const std::map &value) { return; } + if (configuration->autoEventsOnUserProperties == true) { + mutex->unlock(); + flushEvents(); + mutex->lock(); + } + std::map data = {{"app_key", session_params["app_key"].get()}, {"device_id", session_params["device_id"].get()}, {"user_details", session_params["user_details"].dump()}}; requestModule->addRequestToQueue(data); @@ -198,6 +218,12 @@ void Countly::setCustomUserDetails(const std::map &val return; } + if (configuration->autoEventsOnUserProperties == true) { + mutex->unlock(); + flushEvents(); + mutex->lock(); + } + std::map data = {{"app_key", session_params["app_key"].get()}, {"device_id", session_params["device_id"].get()}, {"user_details", session_params["user_details"].dump()}}; requestModule->addRequestToQueue(data); @@ -345,7 +371,7 @@ void Countly::_changeDeviceIdWithoutMerge(const std::string &value) { // send all event to server and end current session of old user flushEvents(); - if(configuration->manualSessionControl == false){ + if (configuration->manualSessionControl == false) { endSession(); } @@ -355,10 +381,9 @@ void Countly::_changeDeviceIdWithoutMerge(const std::string &value) { mutex->unlock(); // start a new session for new user - if(configuration->manualSessionControl == false){ + if (configuration->manualSessionControl == false) { beginSession(); } - } #pragma endregion Device Id @@ -439,7 +464,7 @@ void Countly::start(const std::string &app_key, const std::string &host, int por if (!running) { - if(configuration->manualSessionControl == false){ + if (configuration->manualSessionControl == false) { mutex->unlock(); beginSession(); mutex->lock(); @@ -612,7 +637,7 @@ bool Countly::attemptSessionUpdateEQ() { return false; } #endif - if(configuration->manualSessionControl == false){ + if (configuration->manualSessionControl == false) { return !updateSession(); } else { packEvents(); @@ -735,7 +760,7 @@ bool Countly::updateSession() { mutex->lock(); if (began_session == false) { mutex->unlock(); - if(configuration->manualSessionControl == true){ + if (configuration->manualSessionControl == true) { log(LogLevel::WARNING, "[Countly][updateSession] SDK is in manual session control mode and there is no active session. Please start a session first."); return false; } @@ -829,7 +854,7 @@ void Countly::packEvents() { } else { log(LogLevel::DEBUG, "[Countly][packEvents] EQ empty."); } - // report events if there are any to request queue + // report events if there are any to request queue if (!no_events) { sendEventsToRQ(events); } @@ -852,7 +877,6 @@ void Countly::packEvents() { mutex->unlock(); } - void Countly::sendEventsToRQ(const nlohmann::json &events) { log(LogLevel::DEBUG, "[Countly][sendEventsToRQ] Sending events to RQ."); std::map data = {{"app_key", session_params["app_key"].get()}, {"device_id", session_params["device_id"].get()}, {"events", events.dump()}}; @@ -861,7 +885,7 @@ void Countly::sendEventsToRQ(const nlohmann::json &events) { bool Countly::endSession() { log(LogLevel::INFO, "[Countly][endSession]"); - if(began_session == false) { + if (began_session == false) { log(LogLevel::DEBUG, "[Countly][endSession] There is no active session to end."); return true; } diff --git a/tests/event_queue.cpp b/tests/event_queue.cpp index 8bddb3b..44b7d7e 100644 --- a/tests/event_queue.cpp +++ b/tests/event_queue.cpp @@ -234,4 +234,93 @@ TEST_CASE("Tests that sets 'setEventsToRQThreshold' before and after SDK starts" nlohmann::json events = nlohmann::json::parse(oldest_call.data["events"]); CHECK(events.size() == 3); } +} + +TEST_CASE("Tests that saving user details trigger flushing EQ"){ + clearSDK(); + Countly &countly = Countly::getInstance(); + + // Automatic saving of events before user props calls + SUBCASE("Saving user properties should flush EQ") { + countly.enableManualSessionControl(); + test_utils::initCountlyWithFakeNetworking(true, countly); + + test_utils::generateEvents(4, countly); + CHECK(countly.checkEQSize() == 4); + + // set user properties, this should flush the EQ + countly.setUserDetails({{"name", "Full name"}}); + CHECK(countly.checkEQSize() == 0); + + test_utils::generateEvents(4, countly); + CHECK(countly.checkEQSize() == 4); + + // set custom user properties, this should flush the EQ + countly.setCustomUserDetails({{"custom_key", "custom_value"}}); + CHECK(countly.checkEQSize() == 0); + + // RQ should have 4 events and user details + // trigger RQ to send requests to http_call_queue + countly.processRQDebug(); + // queue should have 4 requests + CHECK(!http_call_queue.empty()); + CHECK(http_call_queue.size() == 4); + HTTPCall eventsReq1 = http_call_queue.front(); + http_call_queue.pop_front(); + HTTPCall userDetails = http_call_queue.front(); + http_call_queue.pop_front(); + HTTPCall eventsReq2 = http_call_queue.front(); + http_call_queue.pop_front(); + HTTPCall customUserDetails = http_call_queue.front(); + http_call_queue.pop_front(); + CHECK(http_call_queue.size() == 0); + + // last call should have 4 events + nlohmann::json events1 = nlohmann::json::parse(eventsReq1.data["events"]); + CHECK(events1.size() == 4); + nlohmann::json userDetailsJson = nlohmann::json::parse(userDetails.data["user_details"]); + CHECK(userDetailsJson["name"] == "Full name"); + + nlohmann::json events2 = nlohmann::json::parse(eventsReq2.data["events"]); + CHECK(events2.size() == 4); + nlohmann::json customUserDetailsJson = nlohmann::json::parse(customUserDetails.data["user_details"]); + CHECK(customUserDetailsJson["custom"]["custom_key"] == "custom_value"); + } + + // Automatic saving of events before user props calls + SUBCASE("Saving user properties should not flush EQ when behavior is disabled") { + countly.enableManualSessionControl(); + countly.disableAutoEventsOnUserProperties(); + test_utils::initCountlyWithFakeNetworking(true, countly); + + test_utils::generateEvents(4, countly); + CHECK(countly.checkEQSize() == 4); + + // set user properties, this should flush the EQ + countly.setUserDetails({{"name", "Full name"}}); + CHECK(countly.checkEQSize() == 4); + + test_utils::generateEvents(4, countly); + CHECK(countly.checkEQSize() == 8); + + // set custom user properties, this should flush the EQ + countly.setCustomUserDetails({{"custom_key", "custom_value"}}); + CHECK(countly.checkEQSize() == 8); + // RQ should have 4 events and user details + // trigger RQ to send requests to http_call_queue + countly.processRQDebug(); + // queue should have 2 requests + CHECK(!http_call_queue.empty()); + CHECK(http_call_queue.size() == 2); + HTTPCall userDetails = http_call_queue.front(); + http_call_queue.pop_front(); + HTTPCall customUserDetails = http_call_queue.front(); + http_call_queue.pop_front(); + CHECK(http_call_queue.size() == 0); + + nlohmann::json userDetailsJson = nlohmann::json::parse(userDetails.data["user_details"]); + CHECK(userDetailsJson["name"] == "Full name"); + nlohmann::json customUserDetailsJson = nlohmann::json::parse(customUserDetails.data["user_details"]); + CHECK(customUserDetailsJson["custom"]["custom_key"] == "custom_value"); + } } \ No newline at end of file