From a9bc177777a8dd9a11e0cfb5e5ae96b7c1ff50f0 Mon Sep 17 00:00:00 2001 From: a-maurice Date: Wed, 5 Aug 2026 17:53:15 -0700 Subject: [PATCH 1/4] [Remote Config] Add Custom Signals --- remote_config/src/FirebaseRemoteConfig.cs | 27 ++++++++++++ remote_config/src/swig/remote_config.i | 18 ++++++++ .../Firebase/Sample/RemoteConfig/UIHandler.cs | 21 +++++++++- .../Sample/RemoteConfig/UIHandlerAutomated.cs | 42 +++++++++++++++++++ 4 files changed, 107 insertions(+), 1 deletion(-) diff --git a/remote_config/src/FirebaseRemoteConfig.cs b/remote_config/src/FirebaseRemoteConfig.cs index d70d18161..18da4fa28 100644 --- a/remote_config/src/FirebaseRemoteConfig.cs +++ b/remote_config/src/FirebaseRemoteConfig.cs @@ -252,6 +252,33 @@ public Task SetDefaultsAsync(IDictionary defaults) { RemoteConfigUtil.ConvertDictionaryToMap(defaults)); } + /// @brief Sets the custom signals values based on the input dictionary. + /// + /// @note This upserts the custom signals, i.e., inserts a new signal if none exists + /// or updates the signal if it does. + /// + /// @param customSignals IDictionary of string keys to object values, representing the + /// set of custom signals to apply. Supported value types are string, long (and integer types), + /// double (and float types), or null to remove a signal. + /// If the same key is specified multiple times, the value associated with the last + /// duplicate key is applied. + /// + /// @return A Task which can be used to determine when the operation is + /// complete. + public Task SetCustomSignalsAsync(IDictionary customSignals) { + ThrowIfNull(); + if (customSignals == null) { + throw new System.ArgumentNullException(nameof(customSignals)); + } + StringList signalKeys = new StringList(); + VariantList signalValues = new VariantList(); + foreach (KeyValuePair pair in customSignals) { + signalKeys.Add(pair.Key); + signalValues.Add(Variant.FromObject(pair.Value)); + } + return remoteConfigInternal.SetCustomSignalsInternalAsync(signalKeys, signalValues); + } + /// @brief Asynchronously changes the settings for this Remote Config /// instance. /// diff --git a/remote_config/src/swig/remote_config.i b/remote_config/src/swig/remote_config.i index 4d1880e09..a6a433250 100644 --- a/remote_config/src/swig/remote_config.i +++ b/remote_config/src/swig/remote_config.i @@ -37,6 +37,7 @@ #include #include "app/src/callback.h" #include "app/src/cpp_instance_manager.h" +#include "app/src/log.h" #include "remote_config/src/include/firebase/remote_config.h" namespace firebase { @@ -135,6 +136,7 @@ void SetConfigUpdateCallback(RemoteConfig* rc, firebase::remote_config::ConfigUp %ignore firebase::remote_config::RemoteConfig::GetInstance; %ignore firebase::remote_config::RemoteConfig::SetDefaults; +%ignore firebase::remote_config::RemoteConfig::SetCustomSignals; // Ignore the various Get, as GetValue is used instead. %ignore firebase::remote_config::RemoteConfig::GetBoolean; %ignore firebase::remote_config::RemoteConfig::GetLong; @@ -280,6 +282,22 @@ void SetConfigUpdateCallback(firebase::remote_config::RemoteConfig* rc, delete[] default_array; return future_result; } + + Future SetCustomSignalsInternal( + std::vector keys, + std::vector values) { + if (keys.size() != values.size()) { + firebase::LogError( + "SetCustomSignalsInternal given different list sizes (%zu, %zu)", + keys.size(), values.size()); + return firebase::Future(); + } + std::map custom_signals; + for (size_t i = 0; i < keys.size(); ++i) { + custom_signals[keys[i]] = values[i]; + } + return self->SetCustomSignals(custom_signals); + } } // 314: Ignore warnings about the internal namespace being renamed to diff --git a/remote_config/testapp/Assets/Firebase/Sample/RemoteConfig/UIHandler.cs b/remote_config/testapp/Assets/Firebase/Sample/RemoteConfig/UIHandler.cs index 2fec7aa95..3675a2ba1 100644 --- a/remote_config/testapp/Assets/Firebase/Sample/RemoteConfig/UIHandler.cs +++ b/remote_config/testapp/Assets/Firebase/Sample/RemoteConfig/UIHandler.cs @@ -192,7 +192,23 @@ void FetchComplete(Task fetchTask) { } } - + public Task SetCustomSignalsAsync() { + DebugLog("Setting custom signals..."); + var customSignals = new System.Collections.Generic.Dictionary { + { "item_id", "sword_123" }, + { "player_level", 42 }, + { "item_rating", 4.5 } + }; + return Firebase.RemoteConfig.FirebaseRemoteConfig.DefaultInstance + .SetCustomSignalsAsync(customSignals) + .ContinueWithOnMainThread(task => { + if (task.IsFaulted) { + DebugLog("Failed to set custom signals: " + task.Exception); + } else { + DebugLog("Custom signals set successfully!"); + } + }); + } // Output text to the debug log text field, as well as the console. public void DebugLog(string s) { @@ -234,6 +250,9 @@ void GUIDisplayControls() { if (GUILayout.Button("Display All Keys")) { DisplayAllKeys(); } + if (GUILayout.Button("Set Custom Signals")) { + SetCustomSignalsAsync(); + } if (GUILayout.Button("Fetch Remote Data")) { FetchDataAsync(); } diff --git a/remote_config/testapp/Assets/Firebase/Sample/RemoteConfig/UIHandlerAutomated.cs b/remote_config/testapp/Assets/Firebase/Sample/RemoteConfig/UIHandlerAutomated.cs index 48da93be4..11e233447 100644 --- a/remote_config/testapp/Assets/Firebase/Sample/RemoteConfig/UIHandlerAutomated.cs +++ b/remote_config/testapp/Assets/Firebase/Sample/RemoteConfig/UIHandlerAutomated.cs @@ -15,6 +15,8 @@ protected override void Start() { // non-static. Func[] tests = { TestSetConfigSettings, + TestSetCustomSignals, + TestSetCustomSignalsValidation, TestDisplayData, TestDisplayAllKeys, // Skip the Realtime RC test on desktop as it is not yet supported. @@ -160,5 +162,45 @@ Task TestFetchData() { FirebaseRemoteConfig.DefaultInstance.GetValue("config_test_bool").BooleanValue); }); } + + Task TestSetCustomSignals() { + var signals = new System.Collections.Generic.Dictionary { + { "test_string", "alpha" }, + { "test_int", 42 }, + { "test_double", 3.14 }, + { "test_null", null } + }; + return FirebaseRemoteConfig.DefaultInstance.SetCustomSignalsAsync(signals) + .ContinueWithOnMainThread(task => { + if (task.IsFaulted) { + throw new Exception("SetCustomSignalsAsync failed: " + task.Exception); + } + DebugLog("TestSetCustomSignals passed!"); + }); + } + + Task TestSetCustomSignalsValidation() { + // Null dictionary check + try { + FirebaseRemoteConfig.DefaultInstance.SetCustomSignalsAsync(null); + throw new Exception("Expected ArgumentNullException for null customSignals"); + } catch (ArgumentNullException) { + // Expected + } + + // Invalid value type check + try { + var invalidSignals = new System.Collections.Generic.Dictionary { + { "bad_key", new System.DateTime() } + }; + FirebaseRemoteConfig.DefaultInstance.SetCustomSignalsAsync(invalidSignals); + throw new Exception("Expected ArgumentException for invalid value type"); + } catch (ArgumentException) { + // Expected + } + + DebugLog("TestSetCustomSignalsValidation passed!"); + return Task.FromResult(true); + } } } From 26fa4498566c56f4196c30a308050e5baacef570 Mon Sep 17 00:00:00 2001 From: a-maurice Date: Wed, 12 Aug 2026 13:54:52 -0700 Subject: [PATCH 2/4] Update UIHandlerAutomated.cs --- .../Sample/RemoteConfig/UIHandlerAutomated.cs | 49 ++++++------------- 1 file changed, 16 insertions(+), 33 deletions(-) diff --git a/remote_config/testapp/Assets/Firebase/Sample/RemoteConfig/UIHandlerAutomated.cs b/remote_config/testapp/Assets/Firebase/Sample/RemoteConfig/UIHandlerAutomated.cs index 11e233447..cf3cfcc9c 100644 --- a/remote_config/testapp/Assets/Firebase/Sample/RemoteConfig/UIHandlerAutomated.cs +++ b/remote_config/testapp/Assets/Firebase/Sample/RemoteConfig/UIHandlerAutomated.cs @@ -15,8 +15,6 @@ protected override void Start() { // non-static. Func[] tests = { TestSetConfigSettings, - TestSetCustomSignals, - TestSetCustomSignalsValidation, TestDisplayData, TestDisplayAllKeys, // Skip the Realtime RC test on desktop as it is not yet supported. @@ -25,6 +23,7 @@ protected override void Start() { TestAddAndRemoveConfigUpdateListener, #endif // !(UNITY_IOS || UNITY_TVOS || UNITY_ANDROID) || UNITY_EDITOR TestFetchData, + TestSetCustomSignals, }; testRunner = AutomatedTestRunner.CreateTestRunner( testsToRun: tests, @@ -163,44 +162,28 @@ Task TestFetchData() { }); } - Task TestSetCustomSignals() { + async Task TestSetCustomSignals() { var signals = new System.Collections.Generic.Dictionary { { "test_string", "alpha" }, { "test_int", 42 }, - { "test_double", 3.14 }, - { "test_null", null } + { "test_double", 3.14 } }; - return FirebaseRemoteConfig.DefaultInstance.SetCustomSignalsAsync(signals) - .ContinueWithOnMainThread(task => { - if (task.IsFaulted) { - throw new Exception("SetCustomSignalsAsync failed: " + task.Exception); - } - DebugLog("TestSetCustomSignals passed!"); - }); - } + await FirebaseRemoteConfig.DefaultInstance.SetCustomSignalsAsync(signals); - Task TestSetCustomSignalsValidation() { - // Null dictionary check - try { - FirebaseRemoteConfig.DefaultInstance.SetCustomSignalsAsync(null); - throw new Exception("Expected ArgumentNullException for null customSignals"); - } catch (ArgumentNullException) { - // Expected - } + // After setting the Custom Signal, fetch to get the signal data + await FetchDataAsync(); - // Invalid value type check - try { - var invalidSignals = new System.Collections.Generic.Dictionary { - { "bad_key", new System.DateTime() } - }; - FirebaseRemoteConfig.DefaultInstance.SetCustomSignalsAsync(invalidSignals); - throw new Exception("Expected ArgumentException for invalid value type"); - } catch (ArgumentException) { - // Expected - } + // Because of caching, this isn't the most reliable test, so just print out the value. + DebugLog("Check custom value: " + + FirebaseRemoteConfig.DefaultInstance.GetValue("config_test_with_custom_signal").StringValue); - DebugLog("TestSetCustomSignalsValidation passed!"); - return Task.FromResult(true); + // Set the custom signals back to null, to clear them out. + var signals2 = new System.Collections.Generic.Dictionary { + { "test_string", null }, + { "test_int", null }, + { "test_double", null } + }; + await FirebaseRemoteConfig.DefaultInstance.SetCustomSignalsAsync(signals2); } } } From 9f44fd73be0333c5b1b2bff6bbe4f8dba0a7ef76 Mon Sep 17 00:00:00 2001 From: a-maurice Date: Wed, 12 Aug 2026 14:00:05 -0700 Subject: [PATCH 3/4] Update readme.md --- docs/readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/readme.md b/docs/readme.md index b4955cc8b..d26714fcc 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -113,6 +113,7 @@ Release Notes - Changes - Messaging: Added new Registration methods using Installation Ids. Deprecated old Token based methods. + - Remote Config: Add support for setting Custom Signals. ### 13.15.0 - Changes From 79c4411baeae7f74cf835a2d30c3611611e60e36 Mon Sep 17 00:00:00 2001 From: a-maurice Date: Wed, 12 Aug 2026 15:08:12 -0700 Subject: [PATCH 4/4] Address feedback --- remote_config/src/FirebaseRemoteConfig.cs | 7 +++++-- .../Firebase/Sample/RemoteConfig/UIHandlerAutomated.cs | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/remote_config/src/FirebaseRemoteConfig.cs b/remote_config/src/FirebaseRemoteConfig.cs index 18da4fa28..7ffc5165c 100644 --- a/remote_config/src/FirebaseRemoteConfig.cs +++ b/remote_config/src/FirebaseRemoteConfig.cs @@ -270,9 +270,12 @@ public Task SetCustomSignalsAsync(IDictionary customSignals) { if (customSignals == null) { throw new System.ArgumentNullException(nameof(customSignals)); } - StringList signalKeys = new StringList(); - VariantList signalValues = new VariantList(); + using StringList signalKeys = new StringList(); + using VariantList signalValues = new VariantList(); foreach (KeyValuePair pair in customSignals) { + if (pair.Key == null) { + throw new System.ArgumentException("Key cannot be null", nameof(customSignals)); + } signalKeys.Add(pair.Key); signalValues.Add(Variant.FromObject(pair.Value)); } diff --git a/remote_config/testapp/Assets/Firebase/Sample/RemoteConfig/UIHandlerAutomated.cs b/remote_config/testapp/Assets/Firebase/Sample/RemoteConfig/UIHandlerAutomated.cs index cf3cfcc9c..9f4653e89 100644 --- a/remote_config/testapp/Assets/Firebase/Sample/RemoteConfig/UIHandlerAutomated.cs +++ b/remote_config/testapp/Assets/Firebase/Sample/RemoteConfig/UIHandlerAutomated.cs @@ -171,7 +171,8 @@ async Task TestSetCustomSignals() { await FirebaseRemoteConfig.DefaultInstance.SetCustomSignalsAsync(signals); // After setting the Custom Signal, fetch to get the signal data - await FetchDataAsync(); + await FirebaseRemoteConfig.DefaultInstance.FetchAsync(TimeSpan.Zero); + await FirebaseRemoteConfig.DefaultInstance.ActivateAsync(); // Because of caching, this isn't the most reliable test, so just print out the value. DebugLog("Check custom value: " +