Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add logic for Analytics parameters with maps #1660

Merged
merged 10 commits into from
Oct 18, 2024
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
26 changes: 26 additions & 0 deletions analytics/integration_test/src/integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,32 @@ TEST_F(FirebaseAnalyticsTest, TestLogEventWithMultipleParameters) {
sizeof(kLevelUpParameters) / sizeof(kLevelUpParameters[0]));
}

TEST_F(FirebaseAnalyticsTest, TestLogEventWithComplexParameters) {
// Define the items that will go into the kParameterItems list.
firebase::Variant first_item = firebase::Variant::EmptyMap();
first_item.map()[firebase::analytics::kParameterItemID] = "SKU_12345";
first_item.map()[firebase::analytics::kParameterItemName] = "Horse Armor DLC";
firebase::Variant second_item = firebase::Variant::EmptyMap();
second_item.map()[firebase::analytics::kParameterItemID] = "SKU_67890";
second_item.map()[firebase::analytics::kParameterItemName] =
"Gold Horse Armor DLC";

// Define the parameters that are sent with the ViewCart event.
const firebase::analytics::Parameter kViewCartParameters[] = {
firebase::analytics::Parameter(firebase::analytics::kParameterCurrency,
"USD"),
firebase::analytics::Parameter(firebase::analytics::kParameterValue,
30.03),
firebase::analytics::Parameter(
firebase::analytics::kParameterItems,
std::vector<firebase::Variant>{first_item, second_item}),
};

firebase::analytics::LogEvent(
firebase::analytics::kEventViewCart, kViewCartParameters,
sizeof(kViewCartParameters) / sizeof(kViewCartParameters[0]));
}

TEST_F(FirebaseAnalyticsTest, TestSetConsent) {
// On Android, this test must be performed at the end, after all the tests for
// session ID and instance ID. This is because once you call SetConsent to
Expand Down
125 changes: 106 additions & 19 deletions analytics/src/analytics_android.cc
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,109 @@ void AddToBundle(JNIEnv* env, jobject bundle, const char* key, int64_t value) {
env->DeleteLocalRef(key_string);
}

// Add an ArrayList to the given Bundle.
void AddArrayListToBundle(JNIEnv* env, jobject bundle, const char* key,
jobject arraylist) {
jstring key_string = env->NewStringUTF(key);
env->CallVoidMethod(
bundle, util::bundle::GetMethodId(util::bundle::kPutParcelableArrayList),
key_string, arraylist);
util::CheckAndClearJniExceptions(env);
env->DeleteLocalRef(key_string);
}

// Add a Bundle to the given Bundle.
void AddBundleToBundle(JNIEnv* env, jobject bundle, const char* key,
jobject inner_bundle) {
jstring key_string = env->NewStringUTF(key);
env->CallVoidMethod(bundle,
util::bundle::GetMethodId(util::bundle::kPutBundle),
key_string, inner_bundle);
util::CheckAndClearJniExceptions(env);
env->DeleteLocalRef(key_string);
}

// Declared here so that it can be used, defined below.
jobject MapToBundle(JNIEnv* env, const std::map<Variant, Variant>& map);

// Converts the given vector into a Java ArrayList. It is up to the
// caller to delete the local reference when done.
jobject VectorOfMapsToArrayList(JNIEnv* env,
const std::vector<Variant>& vector) {
jobject arraylist = env->NewObject(
util::array_list::GetClass(),
util::array_list::GetMethodId(util::array_list::kConstructor));

for (const Variant& element : vector) {
if (element.is_map()) {
jobject bundle = MapToBundle(env, element.map());
env->CallBooleanMethod(
arraylist, util::array_list::GetMethodId(util::array_list::kAdd),
bundle);
util::CheckAndClearJniExceptions(env);
env->DeleteLocalRef(bundle);
} else {
LogError("VectorOfMapsToArrayList: Unsupported type (%s) within vector.",
Variant::TypeName(element.type()));
}
}
return arraylist;
}

// Converts and adds the Variant to the given Bundle.
bool AddVariantToBundle(JNIEnv* env, jobject bundle, const char* key,
const Variant& value) {
if (value.is_int64()) {
AddToBundle(env, bundle, key, value.int64_value());
} else if (value.is_double()) {
AddToBundle(env, bundle, key, value.double_value());
} else if (value.is_string()) {
AddToBundle(env, bundle, key, value.string_value());
} else if (value.is_bool()) {
// Just use integer 0 or 1.
AddToBundle(env, bundle, key,
value.bool_value() ? static_cast<int64_t>(1L)
: static_cast<int64_t>(0L));
} else if (value.is_null()) {
// Just use integer 0 for null.
AddToBundle(env, bundle, key, static_cast<int64_t>(0L));
} else if (value.is_vector()) {
jobject arraylist = VectorOfMapsToArrayList(env, value.vector());
AddArrayListToBundle(env, bundle, key, arraylist);
env->DeleteLocalRef(arraylist);
} else if (value.is_map()) {
jobject inner_bundle = MapToBundle(env, value.map());
AddBundleToBundle(env, bundle, key, inner_bundle);
env->DeleteLocalRef(inner_bundle);
} else {
// A Variant type that couldn't be handled was passed in.
return false;
}
return true;
}

// Converts the given map into a Java Bundle. It is up to the caller
// to delete the local reference when done.
jobject MapToBundle(JNIEnv* env, const std::map<Variant, Variant>& map) {
jobject bundle =
env->NewObject(util::bundle::GetClass(),
util::bundle::GetMethodId(util::bundle::kConstructor));
for (const auto& pair : map) {
// Only add elements that use a string key
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just making sure there are no numeric keys allowed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, it is currently only strings, and I doubt that'll change any time soon.

if (!pair.first.is_string()) {
continue;
}
if (!AddVariantToBundle(env, bundle, pair.first.string_value(),
pair.second)) {
LogError("MapToBundle: Unsupported type (%s) within map with key %s.",
Variant::TypeName(pair.second.type()),
pair.first.string_value());
}
util::CheckAndClearJniExceptions(env);
}
return bundle;
}

// Log an event with one string parameter.
void LogEvent(const char* name, const char* parameter_name,
const char* parameter_value) {
Expand Down Expand Up @@ -404,27 +507,11 @@ void LogEvent(const char* name, const Parameter* parameters,
LogEvent(env, name, [env, parameters, number_of_parameters](jobject bundle) {
for (size_t i = 0; i < number_of_parameters; ++i) {
const Parameter& parameter = parameters[i];
if (parameter.value.is_int64()) {
AddToBundle(env, bundle, parameter.name, parameter.value.int64_value());
} else if (parameter.value.is_double()) {
AddToBundle(env, bundle, parameter.name,
parameter.value.double_value());
} else if (parameter.value.is_string()) {
AddToBundle(env, bundle, parameter.name,
parameter.value.string_value());
} else if (parameter.value.is_bool()) {
// Just use integer 0 or 1.
AddToBundle(env, bundle, parameter.name,
parameter.value.bool_value() ? static_cast<int64_t>(1L)
: static_cast<int64_t>(0L));
} else if (parameter.value.is_null()) {
// Just use integer 0 for null.
AddToBundle(env, bundle, parameter.name, static_cast<int64_t>(0L));
} else {
// Vector or Map were passed in.
if (!AddVariantToBundle(env, bundle, parameter.name, parameter.value)) {
// A Variant type that couldn't be handled was passed in.
LogError(
"LogEvent(%s): %s is not a valid parameter value type. "
"Container types are not allowed. No event was logged.",
"No event was logged.",
parameter.name, Variant::TypeName(parameter.value.type()));
}
}
Expand Down
84 changes: 66 additions & 18 deletions analytics/src/analytics_ios.mm
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,69 @@ void LogEvent(const char* name) {
[FIRAnalytics logEventWithName:@(name) parameters:@{}];
}

// Declared here so that it can be used, defined below.
NSDictionary* MapToDictionary(const std::map<Variant, Variant>& map);

// Converts the given vector of Maps into an ObjC NSArray of ObjC NSDictionaries.
NSArray* VectorOfMapsToArray(const std::vector<Variant>& vector) {
NSMutableArray* array = [NSMutableArray arrayWithCapacity:vector.size()];
for (const Variant& element : vector) {
if (element.is_map()) {
NSDictionary* dict = MapToDictionary(element.map());
[array addObject:dict];
} else {
LogError("VectorOfMapsToArray: Unsupported type (%s) within vector.",
Variant::TypeName(element.type()));
}
}
return array;
}

// Converts and adds the Variant to the given Dictionary.
bool AddVariantToDictionary(NSMutableDictionary* dict, NSString* key, const Variant& value) {
if (value.is_int64()) {
[dict setObject:[NSNumber numberWithLongLong:value.int64_value()] forKey:key];
} else if (value.is_double()) {
[dict setObject:[NSNumber numberWithDouble:value.double_value()] forKey:key];
} else if (value.is_string()) {
[dict setObject:SafeString(value.string_value()) forKey:key];
} else if (value.is_bool()) {
// Just use integer 0 or 1.
[dict setObject:[NSNumber numberWithLongLong:value.bool_value() ? 1 : 0] forKey:key];
} else if (value.is_null()) {
// Just use integer 0 for null.
[dict setObject:[NSNumber numberWithLongLong:0] forKey:key];
} else if (value.is_vector()) {
NSArray* array = VectorOfMapsToArray(value.vector());
[dict setObject:array forKey:key];
} else if (value.is_map()) {
NSDictionary* inner_dict = MapToDictionary(value.map());
[dict setObject:inner_dict forKey:key];
} else {
// A Variant type that couldn't be handled was passed in.
return false;
}
return true;
}

// Converts the given map into an ObjC dictionary of ObjC objects.
NSDictionary* MapToDictionary(const std::map<Variant, Variant>& map) {
NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithCapacity:map.size()];
for (const auto& pair : map) {
// Only add elements that use a string key
if (!pair.first.is_string()) {
continue;
}
NSString* key = SafeString(pair.first.string_value());
const Variant& value = pair.second;
if (!AddVariantToDictionary(dict, key, value)) {
LogError("MapToDictionary: Unsupported type (%s) within map with key %s.",
Variant::TypeName(value.type()), key);
}
}
return dict;
}

// Log an event with associated parameters.
void LogEvent(const char* name, const Parameter* parameters, size_t number_of_parameters) {
FIREBASE_ASSERT_RETURN_VOID(internal::IsInitialized());
Expand All @@ -239,25 +302,10 @@ void LogEvent(const char* name, const Parameter* parameters, size_t number_of_pa
for (size_t i = 0; i < number_of_parameters; ++i) {
const Parameter& parameter = parameters[i];
NSString* parameter_name = SafeString(parameter.name);
if (parameter.value.is_int64()) {
[parameters_dict setObject:[NSNumber numberWithLongLong:parameter.value.int64_value()]
forKey:parameter_name];
} else if (parameter.value.is_double()) {
[parameters_dict setObject:[NSNumber numberWithDouble:parameter.value.double_value()]
forKey:parameter_name];
} else if (parameter.value.is_string()) {
[parameters_dict setObject:SafeString(parameter.value.string_value()) forKey:parameter_name];
} else if (parameter.value.is_bool()) {
// Just use integer 0 or 1.
[parameters_dict setObject:[NSNumber numberWithLongLong:parameter.value.bool_value() ? 1 : 0]
forKey:parameter_name];
} else if (parameter.value.is_null()) {
// Just use integer 0 for null.
[parameters_dict setObject:[NSNumber numberWithLongLong:0] forKey:parameter_name];
} else {
// Vector or Map were passed in.
if (!AddVariantToDictionary(parameters_dict, parameter_name, parameter.value)) {
// A Variant type that couldn't be handled was passed in.
LogError("LogEvent(%s): %s is not a valid parameter value type. "
"Container types are not allowed. No event was logged.",
"No event was logged.",
parameter.name, Variant::TypeName(parameter.value.type()));
}
}
Expand Down
18 changes: 10 additions & 8 deletions app/src/util_android.h
Original file line number Diff line number Diff line change
Expand Up @@ -491,14 +491,16 @@ METHOD_LOOKUP_DECLARATION(activity, ACTIVITY_METHODS)
// Used to setup the cache of Bundle class method IDs to reduce time spent
// looking up methods by string.
// clang-format off
#define BUNDLE_METHODS(X) \
X(Constructor, "<init>", "()V"), \
X(GetString, "getString", "(Ljava/lang/String;)Ljava/lang/String;"), \
X(KeySet, "keySet", "()Ljava/util/Set;"), \
X(PutFloat, "putFloat", "(Ljava/lang/String;F)V"), \
X(PutLong, "putLong", "(Ljava/lang/String;J)V"), \
X(PutString, "putString", "(Ljava/lang/String;Ljava/lang/String;)V"), \
X(PutBundle, "putBundle", "(Ljava/lang/String;Landroid/os/Bundle;)V")
#define BUNDLE_METHODS(X) \
X(Constructor, "<init>", "()V"), \
X(GetString, "getString", "(Ljava/lang/String;)Ljava/lang/String;"), \
X(KeySet, "keySet", "()Ljava/util/Set;"), \
X(PutFloat, "putFloat", "(Ljava/lang/String;F)V"), \
X(PutLong, "putLong", "(Ljava/lang/String;J)V"), \
X(PutString, "putString", "(Ljava/lang/String;Ljava/lang/String;)V"), \
X(PutBundle, "putBundle", "(Ljava/lang/String;Landroid/os/Bundle;)V"), \
X(PutParcelableArrayList, "putParcelableArrayList", \
"(Ljava/lang/String;Ljava/util/ArrayList;)V")
// clang-format on
METHOD_LOOKUP_DECLARATION(bundle, BUNDLE_METHODS)

Expand Down
2 changes: 2 additions & 0 deletions release_build_files/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,8 @@ code.
### Upcoming Release
- Changes
- General (Android): Reduced minSdkVersion back to 23.
- Analytics: Add support for Parameters of Lists of Dictionaries, needed
by some events such as ViewCart.
- Auth (Android): Setting photo_url to empty string with UpdateUserProfile
clears the field, making it consistent with the other platforms.

Expand Down
Loading