Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## XX.XX.XX
- Added `multiplyCustomUserDetail` for multiplying the numeric value of a custom user property on the server.

## 26.8.1
- ! Minor breaking change ! Builds now stop with a clear error when the SDK headers find a nlohmann/json older than 3.11 first on the include path, instead of mixing two versions of the library.

Expand Down
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ if(COUNTLY_BUILD_TESTS)
${CMAKE_CURRENT_SOURCE_DIR}/tests/sbs.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/internal_limits.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/internal_limits_integration.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/multi_instance.cpp)
${CMAKE_CURRENT_SOURCE_DIR}/tests/multi_instance.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/user_details.cpp)

target_compile_options(countly-tests PRIVATE $<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-g>)
target_compile_definitions(countly-tests PRIVATE COUNTLY_BUILD_TESTS)
Expand Down
13 changes: 13 additions & 0 deletions include/countly.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ class Countly : public cly::CountlyDelegates {
*/
void setCustomUserDetails(const std::map<std::string, std::string> &value);

/**
* Multiplies the numeric value of the user's custom property 'key' by 'value' on the server.
* Call it after start(): each call queues its own request, after any user details still waiting
* for the first session. A call before start(), an empty key, or a value that is not a finite
* number is ignored and logged.
*/
void multiplyCustomUserDetail(const std::string &key, double value);

/*
setCountry is deprecated, please use void `setLocation(const std::string& countryCode, const std::string& city, const std::string& gpsCoordinates, const std::string& ipAddress)` function instead.
*/
Expand Down Expand Up @@ -477,6 +485,11 @@ class Countly : public cly::CountlyDelegates {
*/
void queueUserDetails();

/**
* Queues a request with the given user details for the current device ID. The caller must hold the instance mutex.
*/
void queueUserDetails(const nlohmann::json &userDetails);

std::chrono::steady_clock::duration getSessionDuration(std::chrono::steady_clock::time_point now);

std::chrono::steady_clock::duration getSessionDuration();
Expand Down
95 changes: 62 additions & 33 deletions src/countly.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "countly/storage_module_db.hpp"
#include "countly/storage_module_memory.hpp"
#include <chrono>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <set>
Expand Down Expand Up @@ -104,6 +105,20 @@ void runRemoteConfigFetch(std::shared_ptr<RemoteConfigFetch> fetch) {
fetch->store->values = response.data;
}
}

/**
* Whether an SDK behavior settings blacklist or whitelist lets 'key' through. An empty list lets every key through.
*/
bool isAllowedByFilter(const FilterList<std::set<std::string>> &filter, const std::string &key) {
if (filter.filterList.empty()) {
return true;
}
bool listed = filter.filterList.find(key) != filter.filterList.end();
if (filter.isWhitelist) {
return listed;
}
return !listed;
}
} // namespace

Countly::Countly() {
Expand Down Expand Up @@ -625,24 +640,14 @@ void Countly::setCustomUserDetails(const std::map<std::string, std::string> &val
if (configurationModule) {
lim = configurationModule->getLimits();
auto upFilter = configurationModule->getUserPropertyFilterList();
if (!upFilter.filterList.empty()) {
for (const auto &kv : value) {
bool allowed;
if (upFilter.isWhitelist) {
allowed = (upFilter.filterList.find(kv.first) != upFilter.filterList.end());
} else {
allowed = (upFilter.filterList.find(kv.first) == upFilter.filterList.end());
}
if (allowed) {
customValue[kv.first] = kv.second;
}
}
if (customValue.empty()) {
log(LogLevel::DEBUG, "[Countly] setCustomUserDetails, All user properties were filtered out by SBS user property filter.");
return;
for (const auto &kv : value) {
if (isAllowedByFilter(upFilter, kv.first)) {
customValue[kv.first] = kv.second;
}
} else {
customValue = value;
}
if (customValue.empty() && !upFilter.filterList.empty()) {
log(LogLevel::DEBUG, "[Countly] setCustomUserDetails, All user properties were filtered out by SBS user property filter.");
return;
}
} else {
customValue = value;
Expand Down Expand Up @@ -670,12 +675,47 @@ void Countly::setCustomUserDetails(const std::map<std::string, std::string> &val
queueUserDetails();
}

void Countly::multiplyCustomUserDetail(const std::string &key, double value) {
if (!is_sdk_initialized) {
log(LogLevel::WARNING, "[Countly] multiplyCustomUserDetail, SDK is not initialized.");
return;
}
if (!std::isfinite(value)) {
log(LogLevel::WARNING, "[Countly] multiplyCustomUserDetail, The parameter 'value' must be a finite number. Returning.");
return;
}
const std::string truncatedKey = cly::limits::truncateString(key, configurationModule->getLimits().maxKeyLength);
if (truncatedKey.empty()) {
log(LogLevel::WARNING, "[Countly] multiplyCustomUserDetail, The parameter 'key' can't be empty. Returning.");
return;
}
if (!isAllowedByFilter(configurationModule->getUserPropertyFilterList(), key)) {
log(LogLevel::DEBUG, "[Countly] multiplyCustomUserDetail, User property filtered out by SBS user property filter: [" + key + "]");
return;
}

if (configuration->autoEventsOnUserProperties == true) {
flushEvents();
}

std::lock_guard<std::mutex> lk(*mutex);
// Details set before start() and still waiting for the first session go first, so the server applies them before the multiplication.
if (session_params.contains("user_details")) {
queueUserDetails();
}
queueUserDetails({{"custom", {{truncatedKey, {{"$mul", value}}}}}});
}

void Countly::queueUserDetails() {
std::map<std::string, std::string> data = {{"app_key", session_params["app_key"].get<std::string>()}, {"device_id", session_params["device_id"].get<std::string>()}, {"user_details", session_params["user_details"].dump()}};
requestModule->addRequestToQueue(data);
queueUserDetails(session_params["user_details"]);
session_params.erase("user_details");
}

void Countly::queueUserDetails(const nlohmann::json &userDetails) {
std::map<std::string, std::string> data = {{"app_key", session_params["app_key"].get<std::string>()}, {"device_id", session_params["device_id"].get<std::string>()}, {"user_details", userDetails.dump()}};
requestModule->addRequestToQueue(data);
}

#pragma region User location

void Countly::setCountry(const std::string &country_code) {
Expand Down Expand Up @@ -1072,20 +1112,9 @@ void Countly::addEvent(const cly::Event &event) {
}

// Apply event filter (only for custom events)
if (!isInternalEvent) {
auto filter = configurationModule->getEventFilterList();
if (!filter.filterList.empty()) {
bool blocked = false;
if (filter.isWhitelist) {
blocked = (filter.filterList.find(eventKey) == filter.filterList.end());
} else {
blocked = (filter.filterList.find(eventKey) != filter.filterList.end());
}
if (blocked) {
log(LogLevel::DEBUG, "[Countly] addEvent, event filtered out by SBS event filter: [" + eventKey + "]");
return;
}
}
if (!isInternalEvent && !isAllowedByFilter(configurationModule->getEventFilterList(), eventKey)) {
log(LogLevel::DEBUG, "[Countly] addEvent, event filtered out by SBS event filter: [" + eventKey + "]");
return;
}

// Copy the event so we can apply segmentation filters without modifying the caller's object
Expand Down
Loading
Loading