diff --git a/analytics/integration_test/src/integration_test.cc b/analytics/integration_test/src/integration_test.cc index bedccee516..b2ad112333 100644 --- a/analytics/integration_test/src/integration_test.cc +++ b/analytics/integration_test/src/integration_test.cc @@ -341,4 +341,70 @@ TEST_F(FirebaseAnalyticsTest, TestSetConsent) { did_test_setconsent_ = true; } +TEST_F(FirebaseAnalyticsTest, TestSetDefaultEventParameters) { + LogInfo( + "Testing SetDefaultEventParameters with initial values, then updating " + "with Null."); + + std::map initial_defaults; + initial_defaults["initial_key"] = "initial_value"; + initial_defaults["key_to_be_nulled"] = "text_before_null"; + initial_defaults["numeric_default"] = 12345; + + LogInfo("Setting initial default event parameters."); + firebase::analytics::SetDefaultEventParameters(initial_defaults); + // Log an event that would pick up these defaults. + firebase::analytics::LogEvent("event_with_initial_defaults"); + LogInfo("Logged event_with_initial_defaults."); + ProcessEvents( + 500); // Short pause for event logging, if it matters for backend. + + std::map updated_defaults; + updated_defaults["key_to_be_nulled"] = firebase::Variant::Null(); + updated_defaults["another_key"] = "another_value"; + // "initial_key" should persist if not overwritten. + // "numeric_default" should persist. + + LogInfo( + "Updating default event parameters, setting key_to_be_nulled to Null."); + firebase::analytics::SetDefaultEventParameters(updated_defaults); + // Log an event that would pick up the updated defaults. + firebase::analytics::LogEvent("event_after_nulling_and_adding"); + LogInfo("Logged event_after_nulling_and_adding."); + ProcessEvents(500); + + // For this C++ SDK integration test, we primarily ensure API calls complete. + // Actual parameter presence on logged events would be verified by + // backend/native tests. + LogInfo( + "TestSetDefaultEventParameters completed. Calls were made " + "successfully."); +} + +TEST_F(FirebaseAnalyticsTest, TestClearDefaultEventParameters) { + LogInfo("Testing ClearDefaultEventParameters."); + + std::map defaults_to_clear; + defaults_to_clear["default_one"] = "will_be_cleared"; + defaults_to_clear["default_two"] = 9876; + + LogInfo("Setting default parameters before clearing."); + firebase::analytics::SetDefaultEventParameters(defaults_to_clear); + // Log an event that would pick up these defaults. + firebase::analytics::LogEvent("event_before_global_clear"); + LogInfo("Logged event_before_global_clear."); + ProcessEvents(500); + + LogInfo("Calling ClearDefaultEventParameters."); + firebase::analytics::ClearDefaultEventParameters(); + // Log an event that should not have the previous defaults. + firebase::analytics::LogEvent("event_after_global_clear"); + LogInfo("Logged event_after_global_clear."); + ProcessEvents(500); + + LogInfo( + "TestClearDefaultEventParameters completed. Call was made " + "successfully."); +} + } // namespace firebase_testapp_automated diff --git a/analytics/src/analytics_android.cc b/analytics/src/analytics_android.cc index d1279c6ba7..a2825a2b76 100644 --- a/analytics/src/analytics_android.cc +++ b/analytics/src/analytics_android.cc @@ -58,6 +58,8 @@ static const ::firebase::App* g_app = nullptr; "()Lcom/google/android/gms/tasks/Task;"), \ X(GetSessionId, "getSessionId", \ "()Lcom/google/android/gms/tasks/Task;"), \ + X(SetDefaultEventParameters, "setDefaultEventParameters", \ + "(Landroid/os/Bundle;)V", util::kMethodTypeInstance), \ X(GetInstance, "getInstance", "(Landroid/content/Context;)" \ "Lcom/google/firebase/analytics/FirebaseAnalytics;", \ firebase::util::kMethodTypeStatic) @@ -512,7 +514,8 @@ void LogEvent(const char* name, const Parameter* parameters, LogError( "LogEvent(%s): %s is not a valid parameter value type. " "No event was logged.", - parameter.name, Variant::TypeName(parameter.value.type())); + parameter.name, + firebase::Variant::TypeName(parameter.value.type())); } } }); @@ -609,6 +612,105 @@ void ResetAnalyticsData() { util::CheckAndClearJniExceptions(env); } +void SetDefaultEventParameters( + const std::map& default_parameters) { + FIREBASE_ASSERT_RETURN_VOID(internal::IsInitialized()); + JNIEnv* env = g_app->GetJNIEnv(); + if (!env) return; + + jobject bundle = + env->NewObject(util::bundle::GetClass(), + util::bundle::GetMethodId(util::bundle::kConstructor)); + if (util::CheckAndClearJniExceptions(env) || !bundle) { + LogError("Failed to create Bundle for SetDefaultEventParameters."); + if (bundle) env->DeleteLocalRef(bundle); + return; + } + + for (const auto& pair : default_parameters) { + const Variant& value = pair.second; + const char* key_cstr = pair.first.c_str(); + + if (value.is_null()) { + jstring key_jstring = env->NewStringUTF(key_cstr); + if (util::CheckAndClearJniExceptions(env) || !key_jstring) { + LogError( + "SetDefaultEventParameters: Failed to create jstring for null " + "value key: %s", + key_cstr); + if (key_jstring) env->DeleteLocalRef(key_jstring); + continue; + } + env->CallVoidMethod(bundle, + util::bundle::GetMethodId(util::bundle::kPutString), + key_jstring, nullptr); + if (util::CheckAndClearJniExceptions(env)) { + LogError( + "SetDefaultEventParameters: Failed to put null string for key: %s", + key_cstr); + } + env->DeleteLocalRef(key_jstring); + } else if (value.is_string() || value.is_int64() || value.is_double() || + value.is_bool()) { + // AddVariantToBundle handles these types and their JNI conversions. + // It also logs if an individual AddToBundle within it fails or if a type + // is unsupported by it. + if (!AddVariantToBundle(env, bundle, key_cstr, value)) { + // This specific log gives context that the failure happened during + // SetDefaultEventParameters for a type that was expected to be + // supported by AddVariantToBundle. + LogError( + "SetDefaultEventParameters: Failed to add parameter for key '%s' " + "with supported type '%s'. This might indicate a JNI issue during " + "conversion.", + key_cstr, Variant::TypeName(value.type())); + } + } else if (value.is_vector() || value.is_map()) { + LogError( + "SetDefaultEventParameters: Value for key '%s' has type '%s' which " + "is not supported for default event parameters. Only string, int64, " + "double, bool, and null are supported. Skipping.", + key_cstr, Variant::TypeName(value.type())); + } else { + // This case handles other fundamental Variant types that are not scalars + // and not vector/map. + LogError( + "SetDefaultEventParameters: Value for key '%s' has an unexpected and " + "unsupported type '%s'. Skipping.", + key_cstr, Variant::TypeName(value.type())); + } + } + + env->CallVoidMethod( + g_analytics_class_instance, + analytics::GetMethodId(analytics::kSetDefaultEventParameters), bundle); + if (util::CheckAndClearJniExceptions(env)) { + LogError("Failed to call setDefaultEventParameters on Java instance."); + } + + env->DeleteLocalRef(bundle); +} + +void ClearDefaultEventParameters() { + FIREBASE_ASSERT_RETURN_VOID(internal::IsInitialized()); + JNIEnv* env = g_app->GetJNIEnv(); + if (!env) return; + + // Calling with nullptr bundle should clear the parameters. + env->CallVoidMethod( + g_analytics_class_instance, + analytics::GetMethodId(analytics::kSetDefaultEventParameters), nullptr); + if (util::CheckAndClearJniExceptions(env)) { + // This might happen if the method isn't available on older SDKs, + // or if some other JNI error occurs. + LogError( + "Failed to call setDefaultEventParameters(null) on Java instance. " + "This may indicate the method is not available on this Android SDK " + "version " + "or another JNI error occurred."); + } +} + Future GetAnalyticsInstanceId() { FIREBASE_ASSERT_RETURN(GetAnalyticsInstanceIdLastResult(), internal::IsInitialized()); diff --git a/analytics/src/analytics_ios.mm b/analytics/src/analytics_ios.mm index d56fd61356..9d1982b17e 100644 --- a/analytics/src/analytics_ios.mm +++ b/analytics/src/analytics_ios.mm @@ -306,7 +306,7 @@ void LogEvent(const char* name, const Parameter* parameters, size_t number_of_pa // A Variant type that couldn't be handled was passed in. LogError("LogEvent(%s): %s is not a valid parameter value type. " "No event was logged.", - parameter.name, Variant::TypeName(parameter.value.type())); + parameter.name, firebase::Variant::TypeName(parameter.value.type())); } } [FIRAnalytics logEventWithName:@(name) parameters:parameters_dict]; @@ -373,6 +373,44 @@ void SetSessionTimeoutDuration(int64_t milliseconds) { setSessionTimeoutInterval:static_cast(milliseconds) / kMillisecondsPerSecond]; } +void SetDefaultEventParameters(const std::map& default_parameters) { + FIREBASE_ASSERT_RETURN_VOID(internal::IsInitialized()); + NSMutableDictionary* ns_default_parameters = + [[NSMutableDictionary alloc] initWithCapacity:default_parameters.size()]; + for (const auto& pair : default_parameters) { + NSString* key = SafeString(pair.first.c_str()); + const Variant& value = pair.second; + + if (value.is_null()) { + [ns_default_parameters setObject:[NSNull null] forKey:key]; + } else if (value.is_int64()) { + [ns_default_parameters setObject:[NSNumber numberWithLongLong:value.int64_value()] + forKey:key]; + } else if (value.is_double()) { + [ns_default_parameters setObject:[NSNumber numberWithDouble:value.double_value()] forKey:key]; + } else if (value.is_string()) { + [ns_default_parameters setObject:SafeString(value.string_value()) forKey:key]; + } else if (value.is_bool()) { + [ns_default_parameters setObject:[NSNumber numberWithBool:value.bool_value()] forKey:key]; + } else if (value.is_vector() || value.is_map()) { + LogError("SetDefaultEventParameters: Value for key '%s' has type '%s' which is not supported " + "for default event parameters. Only string, int64, double, bool, and null are " + "supported. Skipping.", + pair.first.c_str(), Variant::TypeName(value.type())); + } else { + LogError("SetDefaultEventParameters: Value for key '%s' has an unexpected type '%s' which is " + "not supported. Skipping.", + pair.first.c_str(), Variant::TypeName(value.type())); + } + } + [FIRAnalytics setDefaultEventParameters:ns_default_parameters]; +} + +void ClearDefaultEventParameters() { + FIREBASE_ASSERT_RETURN_VOID(internal::IsInitialized()); + [FIRAnalytics setDefaultEventParameters:nil]; +} + void ResetAnalyticsData() { MutexLock lock(g_mutex); FIREBASE_ASSERT_RETURN_VOID(internal::IsInitialized()); diff --git a/analytics/src/analytics_stub.cc b/analytics/src/analytics_stub.cc index 37c4b3520b..4ae7410bbc 100644 --- a/analytics/src/analytics_stub.cc +++ b/analytics/src/analytics_stub.cc @@ -142,6 +142,17 @@ void SetSessionTimeoutDuration(int64_t /*milliseconds*/) { FIREBASE_ASSERT_RETURN_VOID(internal::IsInitialized()); } +void SetDefaultEventParameters( + const std::map& /*default_parameters*/) { + FIREBASE_ASSERT_RETURN_VOID(internal::IsInitialized()); + // This is a stub implementation. No operation needed. +} + +void ClearDefaultEventParameters() { + FIREBASE_ASSERT_RETURN_VOID(internal::IsInitialized()); + // This is a stub implementation. No operation needed. +} + void ResetAnalyticsData() { FIREBASE_ASSERT_RETURN_VOID(internal::IsInitialized()); g_fake_instance_id++; diff --git a/analytics/src/include/firebase/analytics.h b/analytics/src/include/firebase/analytics.h index 746df99894..f9bdc64eb0 100644 --- a/analytics/src/include/firebase/analytics.h +++ b/analytics/src/include/firebase/analytics.h @@ -558,6 +558,23 @@ void SetSessionTimeoutDuration(int64_t milliseconds); /// instance id. void ResetAnalyticsData(); +/// @brief Sets the default event parameters. +/// +/// These parameters will be automatically logged with all calls to `LogEvent`. +/// Default parameters are overridden by parameters supplied to the `LogEvent` +/// method. +/// +/// When a value in the `default_parameters` map is +/// `firebase::Variant::Null()`, it signifies that the default parameter for +/// that specific key should be cleared. +/// +/// @param[in] default_parameters A map of parameter names to Variant values. +void SetDefaultEventParameters( + const std::map& default_parameters); + +/// @brief Clears all default event parameters. +void ClearDefaultEventParameters(); + /// Get the instance ID from the analytics service. /// /// @note This is *not* the same ID as the ID returned by diff --git a/analytics/src_ios/fake/FIRAnalytics.h b/analytics/src_ios/fake/FIRAnalytics.h index 50b1b7ca36..90ffb32fb7 100644 --- a/analytics/src_ios/fake/FIRAnalytics.h +++ b/analytics/src_ios/fake/FIRAnalytics.h @@ -37,4 +37,6 @@ + (void)resetAnalyticsData; ++ (void)setDefaultEventParameters:(nullable NSDictionary *)parameters; + @end diff --git a/analytics/src_ios/fake/FIRAnalytics.mm b/analytics/src_ios/fake/FIRAnalytics.mm index 11048e3dd0..64bd1c343d 100644 --- a/analytics/src_ios/fake/FIRAnalytics.mm +++ b/analytics/src_ios/fake/FIRAnalytics.mm @@ -21,6 +21,9 @@ @implementation FIRAnalytics + (NSString *)stringForValue:(id)value { + if (value == [NSNull null]) { + return @""; + } return [NSString stringWithFormat:@"%@", value]; } @@ -94,4 +97,14 @@ + (void)resetAnalyticsData { FakeReporter->AddReport("+[FIRAnalytics resetAnalyticsData]", {}); } ++ (void)setDefaultEventParameters:(nullable NSDictionary *)parameters { + if (parameters == nil) { + FakeReporter->AddReport("+[FIRAnalytics setDefaultEventParameters:]", {"nil"}); + } else { + NSString *parameterString = [self stringForParameters:parameters]; + FakeReporter->AddReport("+[FIRAnalytics setDefaultEventParameters:]", + { [parameterString UTF8String] }); + } +} + @end diff --git a/analytics/src_java/fake/com/google/firebase/analytics/FirebaseAnalytics.java b/analytics/src_java/fake/com/google/firebase/analytics/FirebaseAnalytics.java index 8be63c473d..cf70459b6a 100644 --- a/analytics/src_java/fake/com/google/firebase/analytics/FirebaseAnalytics.java +++ b/analytics/src_java/fake/com/google/firebase/analytics/FirebaseAnalytics.java @@ -79,4 +79,31 @@ public void setSessionTimeoutDuration(long milliseconds) { FakeReporter.addReport( "FirebaseAnalytics.setSessionTimeoutDuration", Long.toString(milliseconds)); } + + public void setDefaultEventParameters(Bundle bundle) { + if (bundle == null) { + FakeReporter.addReport("FirebaseAnalytics.setDefaultEventParameters", "null"); + } else { + StringBuilder paramsString = new StringBuilder(); + // Sort keys for predictable ordering. + for (String key : new TreeSet<>(bundle.keySet())) { + paramsString.append(key); + paramsString.append("="); + Object value = bundle.get(key); // Get as Object first + if (value == null) { + // This case handles when bundle.putString(key, null) was called. + paramsString.append(""); + } else { + paramsString.append(value.toString()); + } + paramsString.append(","); + } + // Remove trailing comma if paramsString is not empty + if (paramsString.length() > 0) { + paramsString.setLength(paramsString.length() - 1); + } + FakeReporter.addReport( + "FirebaseAnalytics.setDefaultEventParameters", paramsString.toString()); + } + } } diff --git a/analytics/tests/analytics_test.cc b/analytics/tests/analytics_test.cc index 8235c69e25..32852667f0 100644 --- a/analytics/tests/analytics_test.cc +++ b/analytics/tests/analytics_test.cc @@ -296,5 +296,101 @@ TEST_F(AnalyticsTest, TestGetAnalyticsInstanceId) { EXPECT_EQ(std::string("FakeAnalyticsInstanceId0"), *result.result()); } +TEST_F(AnalyticsTest, TestSetDefaultEventParameters) { + // Android part is not tested here as it's about iOS fakes. + // This test focuses on the iOS fake reporting. + // Android: + // AddExpectationAndroid("FirebaseAnalytics.setDefaultEventParameters", + // {"my_param_bool=true,my_param_double=3.14,my_param_int=123,my_param_string=hello"}); + AddExpectationApple("+[FIRAnalytics setDefaultEventParameters:]", + {"my_param_bool=1,my_param_double=3.14,my_param_int=123," + "my_param_string=hello"}); + + std::map default_params; + default_params["my_param_string"] = "hello"; + default_params["my_param_double"] = 3.14; + default_params["my_param_int"] = 123; + default_params["my_param_bool"] = + true; // Note: [NSNumber numberWithBool:YES] stringifies to 1 + + SetDefaultEventParameters(default_params); +} + +TEST_F(AnalyticsTest, TestSetDefaultEventParametersWithNull) { + // Android: + // AddExpectationAndroid("FirebaseAnalytics.setDefaultEventParameters", + // {"key_to_clear=null,other_key=value"}); + AddExpectationApple("+[FIRAnalytics setDefaultEventParameters:]", + {"key_to_clear=,other_key=value"}); + + std::map default_params; + default_params["key_to_clear"] = Variant::Null(); + default_params["other_key"] = "value"; + + SetDefaultEventParameters(default_params); +} + +TEST_F(AnalyticsTest, TestSetDefaultEventParametersEmpty) { + // Android: + // AddExpectationAndroid("FirebaseAnalytics.setDefaultEventParameters", {""}); + // // Or however an empty bundle is represented + AddExpectationApple("+[FIRAnalytics setDefaultEventParameters:]", {""}); + + std::map default_params; + SetDefaultEventParameters(default_params); +} + +TEST_F(AnalyticsTest, TestClearDefaultEventParameters) { + // Android: + // AddExpectationAndroid("FirebaseAnalytics.setDefaultEventParameters", + // {"null"}); // Passing null bundle + AddExpectationApple("+[FIRAnalytics setDefaultEventParameters:]", {"nil"}); + + ClearDefaultEventParameters(); +} + +TEST_F(AnalyticsTest, TestSetDefaultEventParametersAndroid) { + AddExpectationAndroid( + "FirebaseAnalytics.setDefaultEventParameters", + {"my_bool=true,my_double=3.14,my_int=123,my_string=hello"}); + + std::map default_params; + default_params["my_string"] = "hello"; + default_params["my_double"] = 3.14; + default_params["my_int"] = 123; + default_params["my_bool"] = true; + + SetDefaultEventParameters(default_params); + WaitForMainThreadTask(); +} + +TEST_F(AnalyticsTest, TestSetDefaultEventParametersWithNullValueAndroid) { + AddExpectationAndroid("FirebaseAnalytics.setDefaultEventParameters", + {"key_to_clear=,other_key=value"}); + + std::map default_params; + default_params["key_to_clear"] = Variant::Null(); + default_params["other_key"] = "value"; + + SetDefaultEventParameters(default_params); + WaitForMainThreadTask(); +} + +TEST_F(AnalyticsTest, TestSetDefaultEventParametersEmptyAndroid) { + AddExpectationAndroid("FirebaseAnalytics.setDefaultEventParameters", {""}); + + std::map default_params; + SetDefaultEventParameters(default_params); + WaitForMainThreadTask(); +} + +TEST_F(AnalyticsTest, TestClearDefaultEventParametersAndroid) { + AddExpectationAndroid("FirebaseAnalytics.setDefaultEventParameters", + {"null"}); + + ClearDefaultEventParameters(); + WaitForMainThreadTask(); +} + } // namespace analytics } // namespace firebase diff --git a/release_build_files/readme.md b/release_build_files/readme.md index 1d52fae82f..41998c1e76 100644 --- a/release_build_files/readme.md +++ b/release_build_files/readme.md @@ -666,6 +666,8 @@ code. be removed soon. - Messaging (Android): Fix issue with the Subscribe Future not completing when a cached token is available. + - Analytics: Added `SetDefaultEventParameters` and + `ClearDefaultEventParameters`. ### 12.7.0 - Changes