diff --git a/.github/workflows/release_notice.yml b/.github/workflows/release_notice.yml new file mode 100644 index 0000000..e17315a --- /dev/null +++ b/.github/workflows/release_notice.yml @@ -0,0 +1,39 @@ +name: Release Notice +on: + release: + types: [published] + workflow_dispatch: +jobs: + build: + runs-on: ubuntu-latest + steps: + # To check the github context + - name: Dump Github context + env: + GITHUB_CONTEXT: ${{ toJSON(github) }} + run: echo "$GITHUB_CONTEXT" + - name: Send custom JSON data to Slack workflow + id: slack + uses: slackapi/slack-github-action@v1.23.0 + with: + # This data can be any valid JSON from a previous step in the GitHub Action + payload: | + { + "repository": "${{ github.repository }}", + "tag_name": "${{ github.event.release.tag_name }}", + "actor": "${{ github.actor }}", + "body": ${{ toJSON(github.event.release.body) }}, + "html_url": "${{ github.event.release.html_url }}" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_RELEASE }} + - name: Send custom JSON data to Discord + uses: sarisia/actions-status-discord@v1.13.0 + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_URL }} + nodetail: true + title: New ${{ github.repository }} version ${{ github.event.release.tag_name }} published by ${{ github.actor }} + description: | + Release URL: ${{ github.event.release.html_url }} + Click [here](https://github.com/Countly/countly-server/blob/master/CHANGELOG.md) to view the change log. + `${{ github.event.release.body }}` diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b498382..80cc101 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,26 +14,26 @@ jobs: fail-fast: false matrix: os: - - ubuntu-20.04 - - macos-11.0 - - windows-2019 + - ubuntu-22.04 + - macos-15 + - windows-2022 include: - - os: windows-2019 - cmake-generator: -G "Visual Studio 16 2019" -A x64 - cmake-install: "choco install -y cmake" - dependencies: | - choco install -y openssl - choco install -y visualstudio2017-workload-vctools - choco upgrade -y visualstudio2017-workload-vctools - make: msbuild countly-tests.vcxproj -t:rebuild -verbosity:diag -property:Configuration=Release && .\Release\countly-tests.exe - - os: macos-11.0 - cmake-install: "brew install cmake" - dependencies: "brew install openssl" + - os: windows-2022 + cmake-generator: -G "Visual Studio 17 2022" -A x64 + cmake-install: "" #Already installed on hosted runner + dependencies: "" #Already installed on hosted runner + make: msbuild countly-tests.vcxproj -t:rebuild -verbosity:diag -property:Configuration=Release && .\Release\countly-tests.exe + - os: macos-15 + cmake-install: "" #Already installed on hosted runner + dependencies: "" #Already installed on hosted runner make: make ./countly-tests && ./countly-tests - - os: ubuntu-20.04 - cmake-install: "sudo apt-get update && sudo apt-get install -y cmake" - dependencies: "sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev" + - os: ubuntu-22.04 + cmake-install: "" #Already installed on hosted runner + dependencies: | + sudo apt-get update && sudo apt-get install -y \ + libcurl4-openssl-dev \ + libssl-dev make: make ./countly-tests && ./countly-tests steps: @@ -52,7 +52,7 @@ jobs: run: ${{ matrix.dependencies }} - name: Set up MSVC - if: matrix.os == 'windows-2019' + if: matrix.os == 'windows-2022' uses: microsoft/setup-msbuild@v1 - name: Build and run tests @@ -60,3 +60,5 @@ jobs: cmake -DCOUNTLY_BUILD_TESTS=1 -B build . ${{ matrix.cmake-generator }} cd build ${{ matrix.make }} + env: + CMAKE_POLICY_VERSION_MINIMUM: 3.31 diff --git a/CHANGELOG.md b/CHANGELOG.md index d51c0f2..654f754 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 23.2.2 +- Mitigated a mutex issue that can happen during update loop. + +## 23.2.1 +- Added manual session control via "Countly::enableManualSessionControl". When enabled, automatic session calls are ignored, while manual calls remain usable for finer control. +- Added "checkRQSize" function to return the current number of requests in the queue. + ## 23.2.0 - Request queue processing now is limited to 100 requests at a time - Added 'setEventsToRQThreshold' method that sets the number of events after which all events will be sent to the RQ. Default value is set to 100. diff --git a/README.md b/README.md index a5338d2..a272183 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Codacy Badge](https://app.codacy.com/project/badge/Grade/f3268a85b0034b68aa4fc47c9dce596c)](https://www.codacy.com/gh/Countly/countly-sdk-cpp/dashboard?utm_source=github.com&utm_medium=referral&utm_content=Countly/countly-sdk-cpp&utm_campaign=Badge_Grade) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/f3268a85b0034b68aa4fc47c9dce596c)](https://app.codacy.com/gh/Countly/countly-sdk-cpp/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) # Countly C++ SDK diff --git a/examples/example_integration.cpp b/examples/example_integration.cpp index 1497337..5e7410d 100644 --- a/examples/example_integration.cpp +++ b/examples/example_integration.cpp @@ -30,6 +30,115 @@ void printLog(LogLevel level, const string &msg) { cout << lvl << msg << endl; } +// // Callback function to write response data +// static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) { +// ((std::string*)userp)->append((char*)contents, size * nmemb); +// return size * nmemb; +// } + +// // Custom HTTP client for macOS +// HTTPResponse customClient(bool use_post, const std::string &path, const std::string &data) { +// HTTPResponse response; +// response.success = false; +// cout << "Making real HTTP request to: " << path << endl; + +// CURL *curl; +// CURLcode res; +// std::string readBuffer; + +// curl = curl_easy_init(); +// if(curl) { +// // Set URL +// curl_easy_setopt(curl, CURLOPT_URL, path.c_str()); + +// // Set callback function to write data +// curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); +// curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); + +// // Set timeout +// curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); + +// // Follow redirects +// curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + +// // SSL verification (set to 0 for testing, 1 for production) +// curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); +// curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + +// // Set User-Agent +// curl_easy_setopt(curl, CURLOPT_USERAGENT, "Countly-SDK-CPP/1.0"); + +// if (use_post) { +// // Set POST method +// curl_easy_setopt(curl, CURLOPT_POST, 1L); + +// if (!data.empty()) { +// // Set POST data +// curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str()); +// curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, data.length()); +// } + +// // Set content type for POST +// struct curl_slist *headers = NULL; +// headers = curl_slist_append(headers, "Content-Type: application/x-www-form-urlencoded"); +// curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); +// } else { +// // For GET requests, append data as query parameters +// if (!data.empty()) { +// std::string fullUrl = path; +// fullUrl += (path.find('?') != std::string::npos) ? "&" : "?"; +// fullUrl += data; +// curl_easy_setopt(curl, CURLOPT_URL, fullUrl.c_str()); +// } +// } + +// // Perform the request +// res = curl_easy_perform(curl); + +// if(res != CURLE_OK) { +// cout << "curl_easy_perform() failed: " << curl_easy_strerror(res) << endl; +// } else { +// // Get response code +// long response_code; +// curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + +// cout << "HTTP Response Code: " << response_code << endl; +// cout << "Raw response: " << readBuffer << endl; + +// // Check if HTTP request was successful (2xx status codes) +// if (response_code >= 200 && response_code < 300) { +// // Check if response contains { result: 'Success' } or {"result":"Success"} +// if (readBuffer.find("\"result\"") != std::string::npos && +// (readBuffer.find("\"Success\"") != std::string::npos || +// readBuffer.find("'Success'") != std::string::npos)) { +// response.success = true; +// cout << "Success response detected!" << endl; +// } else { +// cout << "Response does not indicate success" << endl; +// } + +// // Parse as JSON +// try { +// response.data = nlohmann::json::parse(readBuffer); +// } catch (const std::exception& e) { +// cout << "Failed to parse JSON response: " << e.what() << endl; +// response.data = nlohmann::json::object(); +// } +// } else { +// cout << "HTTP request failed with code: " << response_code << endl; +// } +// } + +// // Cleanup +// curl_easy_cleanup(curl); +// } else { +// cout << "Failed to initialize curl" << endl; +// } + +// cout << "Request completed. Success: " << (response.success ? "true" : "false") << endl; +// return response; +// } + int main() { cout << "Sample App" << endl; Countly &ct = Countly::getInstance(); @@ -38,7 +147,10 @@ int main() { // Please refer to the documentation for more information: // https://support.count.ly/hc/en-us/articles/4416163384857-C- - ct.alwaysUsePost(true); + // Custom HTTP client + // HTTPClientFunction clientPtr = customClient; + // ct.setHTTPClient(clientPtr); + // ct.alwaysUsePost(true); ct.setLogger(printLog); ct.SetPath("databaseFileName.db"); // this will be only built into account if the correct configurations are set ct.setDeviceID("test-device-id"); @@ -47,7 +159,14 @@ int main() { ct.SetMetrics("Windows 10", "10.22", "Mac", "800x600", "Carrier", "1.0"); // start the SDK (initialize the SDK) - ct.start("YOUR_APP_KEY", "https://try.count.ly", 443, true); + string _appKey = "YOUR_APP_KEY"; + string _serverUrl = "https://your.server.ly"; + + if(_appKey.compare("YOUR_APP_KEY") == 0 || _serverUrl.compare("https://your.server.ly") == 0) { + cerr << "Please do not use default set of app key and server url" << endl; + } + + ct.start(_appKey, _serverUrl, 443, true); ct.setAutomaticSessionUpdateInterval(5);// The value is set so low just for internal validation. ct.setMaxRQProcessingBatchSize(2); // in most cases not needed to be set. The value is set so low just for internal validation diff --git a/include/countly.hpp b/include/countly.hpp index 9c95583..fa985e3 100644 --- a/include/countly.hpp +++ b/include/countly.hpp @@ -56,6 +56,8 @@ class Countly : public cly::CountlyDelegates { void setSha256(cly::SHA256Function fun); + void enableManualSessionControl(); + 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); @@ -105,6 +107,11 @@ class Countly : public cly::CountlyDelegates { */ int checkEQSize(); + /* + * Checks and returns the size of the request queue in memory or persistent storage. + */ + int checkRQSize(); + /** * Checks and returns the size of the event queue in persistent storage. */ @@ -319,6 +326,7 @@ class Countly : public cly::CountlyDelegates { std::chrono::system_clock::duration getSessionDuration(); void updateLoop(); + void packEvents(); bool began_session = false; bool is_being_disposed = false; bool is_sdk_initialized = false; diff --git a/include/countly/constants.hpp b/include/countly/constants.hpp index 99471ad..300e09e 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.0" +#define COUNTLY_SDK_VERSION "23.2.2" #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 0b4aa4f..c98d500 100644 --- a/include/countly/countly_configuration.hpp +++ b/include/countly/countly_configuration.hpp @@ -68,6 +68,8 @@ struct CountlyConfiguration { SHA256Function sha256_function = nullptr; + bool manualSessionControl = false; + HTTPClientFunction http_client_function = nullptr; nlohmann::json metrics; diff --git a/include/countly/request_module.hpp b/include/countly/request_module.hpp index 5b013e6..7d494a1 100644 --- a/include/countly/request_module.hpp +++ b/include/countly/request_module.hpp @@ -35,6 +35,8 @@ class RequestModule { */ void clearRequestQueue(); + long long RQSize(); + private: class RequestModuleImpl; std::unique_ptr impl; diff --git a/src/countly.cpp b/src/countly.cpp index bb2188f..e5b321a 100644 --- a/src/countly.cpp +++ b/src/countly.cpp @@ -128,6 +128,20 @@ void Countly::setSha256(SHA256Function fun) { mutex->unlock(); } +/** + * Enable manual session handling. + */ +void Countly::enableManualSessionControl() { + if (is_sdk_initialized) { + log(LogLevel::WARNING, "[Countly][enableManualSessionControl] You can not enable manual session control after SDK initialization."); + return; + } + + mutex->lock(); + configuration->manualSessionControl = true; + 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."); @@ -332,17 +346,19 @@ void Countly::_changeDeviceIdWithoutMerge(const std::string &value) { // send all event to server and end current session of old user flushEvents(); - if (began_session) { + if(configuration->manualSessionControl == false){ endSession(); - mutex->lock(); - session_params["device_id"] = value; - mutex->unlock(); + } + + mutex->lock(); + session_params["device_id"] = value; + mutex->unlock(); + + // start a new session for new user + if(configuration->manualSessionControl == false){ beginSession(); - } else { - mutex->lock(); - session_params["device_id"] = value; - mutex->unlock(); } + } #pragma endregion Device Id @@ -357,6 +373,7 @@ void Countly::start(const std::string &app_key, const std::string &host, int por #ifdef COUNTLY_USE_SQLITE if (configuration->databasePath == "" || configuration->databasePath == " ") { log(LogLevel::ERROR, "[Countly][start] Database path can not be empty or blank."); + mutex->unlock(); return; } #endif @@ -422,9 +439,11 @@ void Countly::start(const std::string &app_key, const std::string &host, int por if (!running) { - mutex->unlock(); - beginSession(); - mutex->lock(); + if(configuration->manualSessionControl == false){ + mutex->unlock(); + beginSession(); + mutex->lock(); + } if (start_thread) { stop_thread = false; @@ -451,7 +470,7 @@ void Countly::startOnCloud(const std::string &app_key) { void Countly::stop() { _deleteThread(); - if (began_session) { + if (configuration->manualSessionControl == false) { endSession(); } } @@ -520,7 +539,7 @@ void Countly::checkAndSendEventToRQ() { void Countly::setMaxEvents(size_t value) { log(LogLevel::WARNING, "[Countly][setMaxEvents/SetMaxEventsPerMessage] These calls are deprecated. Use 'setEventsToRQThreshold' instead."); - setEventsToRQThreshold(value); + setEventsToRQThreshold(static_cast(value)); } void Countly::setEventsToRQThreshold(int value) { @@ -593,8 +612,12 @@ bool Countly::attemptSessionUpdateEQ() { return false; } #endif - - return !updateSession(); + if(configuration->manualSessionControl == false){ + return !updateSession(); + } else { + packEvents(); + return false; + } } void Countly::clearEQInternal() { @@ -654,8 +677,9 @@ std::vector Countly::debugReturnStateOfEQ() { bool Countly::beginSession() { mutex->lock(); log(LogLevel::INFO, "[Countly][beginSession]"); - if (began_session) { + if (began_session == true) { mutex->unlock(); + log(LogLevel::DEBUG, "[Countly][beginSession] Session is already active."); return true; } @@ -709,8 +733,12 @@ bool Countly::updateSession() { try { // Check if there was a session, if not try to start one mutex->lock(); - if (!began_session) { + if (began_session == false) { mutex->unlock(); + 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; + } if (!beginSession()) { // if beginSession fails, we should not try to update session return false; @@ -777,6 +805,54 @@ bool Countly::updateSession() { return true; } +void Countly::packEvents() { + try { + mutex->lock(); + // events array + nlohmann::json events = nlohmann::json::array(); + std::string event_ids; + mutex->unlock(); + bool no_events = checkEQSize() > 0 ? false : true; + mutex->lock(); + + if (!no_events) { +#ifndef COUNTLY_USE_SQLITE + for (const auto &event_json : event_queue) { + events.push_back(nlohmann::json::parse(event_json)); + } +#else + // TODO: If database_path was empty there was return false here + mutex->unlock(); + fillEventsIntoJson(events, event_ids); + mutex->lock(); +#endif + } else { + log(LogLevel::DEBUG, "[Countly][packEvents] EQ empty."); + } + // report events if there are any to request queue + if (!no_events) { + sendEventsToRQ(events); + } + +// clear event queue +// TODO: check if we want to totally wipe the event queue in memory but not in database +#ifndef COUNTLY_USE_SQLITE + event_queue.clear(); +#else + if (!event_ids.empty()) { + // this is a partial clearance, we only remove the events that were sent + removeEventWithId(event_ids); + } +#endif + } catch (const std::system_error &e) { + std::ostringstream log_message; + log_message << "packEvents, error: " << e.what(); + log(LogLevel::FATAL, log_message.str()); + } + 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()}}; @@ -785,6 +861,10 @@ void Countly::sendEventsToRQ(const nlohmann::json &events) { bool Countly::endSession() { log(LogLevel::INFO, "[Countly][endSession]"); + if(began_session == false) { + log(LogLevel::DEBUG, "[Countly][endSession] There is no active session to end."); + return true; + } const std::chrono::system_clock::time_point now = Countly::getTimestamp(); const auto timestamp = std::chrono::duration_cast(now.time_since_epoch()); const auto duration = std::chrono::duration_cast(getSessionDuration(now)); @@ -824,12 +904,24 @@ int Countly::checkEQSize() { return event_count; } +int Countly::checkRQSize() { + log(LogLevel::DEBUG, "[Countly][checkRQSize]"); + int request_count = -1; + if (!is_sdk_initialized) { + log(LogLevel::DEBUG, "[Countly][checkRQSize] SDK is not initialized."); + return request_count; + } + + request_count = static_cast(requestModule->RQSize()); + return request_count; +} + #ifndef COUNTLY_USE_SQLITE int Countly::checkMemoryEQSize() { log(LogLevel::DEBUG, "[Countly][checkMemoryEQSize] Checking event queue size in memory"); int result = 0; mutex->lock(); - result = event_queue.size(); + result = static_cast(event_queue.size()); mutex->unlock(); return result; } @@ -1110,8 +1202,10 @@ void Countly::updateLoop() { size_t last_wait_milliseconds = wait_milliseconds; mutex->unlock(); std::this_thread::sleep_for(std::chrono::milliseconds(last_wait_milliseconds)); - if (enable_automatic_session) { + if (enable_automatic_session == true && configuration->manualSessionControl == false) { updateSession(); + } else if (configuration->manualSessionControl == true) { + packEvents(); } requestModule->processQueue(mutex); } @@ -1205,4 +1299,4 @@ void Countly::updateRemoteConfigExcept(std::string *keys, size_t key_count) { std::thread _thread(&Countly::_updateRemoteConfigWithSpecificValues, this, data); _thread.detach(); } -} // namespace cly \ No newline at end of file +} // namespace cly diff --git a/src/request_module.cpp b/src/request_module.cpp index 7b23996..1770c86 100644 --- a/src/request_module.cpp +++ b/src/request_module.cpp @@ -341,4 +341,5 @@ HTTPResponse RequestModule::sendHTTP(std::string path, std::string data) { return response; #endif } +long long RequestModule::RQSize() { return impl->_storageModule->RQCount(); } } // namespace cly diff --git a/tests/config.cpp b/tests/config.cpp index 8949dff..002a590 100644 --- a/tests/config.cpp +++ b/tests/config.cpp @@ -51,6 +51,7 @@ TEST_CASE("Validate setting configuration values") { CHECK(config.breadcrumbsThreshold == 100); CHECK(config.forcePost == false); CHECK(config.port == 443); + CHECK(config.manualSessionControl == false); CHECK(config.sha256_function == nullptr); CHECK(config.http_client_function == nullptr); CHECK(config.metrics.empty()); @@ -76,6 +77,7 @@ TEST_CASE("Validate setting configuration values") { ct.setMaxRequestQueueSize(10); ct.SetPath(TEST_DATABASE_NAME); ct.setMaxRQProcessingBatchSize(10); + ct.enableManualSessionControl(); ct.start("YOUR_APP_KEY", "https://try.count.ly", -1, false); // Get configuration values using Countly getters @@ -94,6 +96,7 @@ TEST_CASE("Validate setting configuration values") { CHECK(config.breadcrumbsThreshold == 100); CHECK(config.forcePost == true); CHECK(config.port == 443); + CHECK(config.manualSessionControl == true); CHECK(config.sha256_function("custom SHA256") == customSha_1_returnValue); HTTPResponse response = config.http_client_function(true, "", ""); diff --git a/tests/main.cpp b/tests/main.cpp index 613ea1e..499eb8b 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -6,6 +6,9 @@ #include #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#ifdef __APPLE__ +#define DOCTEST_CONFIG_NO_BREAK_INTO_DEBUGGER +#endif #include "doctest.h" diff --git a/vendor/doctest b/vendor/doctest index f13a00c..1da23a3 160000 --- a/vendor/doctest +++ b/vendor/doctest @@ -1 +1 @@ -Subproject commit f13a00cc27ed3c1ec4f755572ab7556c4cb01716 +Subproject commit 1da23a3e8119ec5cce4f9388e91b065e20bf06f5