Skip to content

[Remote Config] Add Custom Signals - #1899

Open
a-maurice wants to merge 6 commits into
mainfrom
am-custom_signals
Open

[Remote Config] Add Custom Signals#1899
a-maurice wants to merge 6 commits into
mainfrom
am-custom_signals

Conversation

@a-maurice

Copy link
Copy Markdown
Contributor

Description

Provide details of the change, and generalize the change in the PR title above.

Add support for setting Custom Signals to Remote Config. This also adds some of the remote config unit tests back in.


Testing

Describe how you've tested these changes. Link any manually triggered Integration tests or CPP binary SDK Packaging Github Action workflows, if applicable.

Running integration tests and unit tests locally.


Type of Change

Place an x the applicable box:

  • Bug fix. Add the issue # below if applicable.
  • New feature. A non-breaking change which adds functionality.
  • Other, such as a build process or documentation change.

Notes

  • Bug fixes and feature changes require an update to the Release Notes section of release_build_files/readme.md.
  • Read the contribution guidelines CONTRIBUTING.md.
  • Changes to the public API require an internal API review. If you'd like to help us make Firebase APIs better, please propose your change in a feature request so that we can discuss it together.

});
});
fbb.Finish();
const std::vector<uint8_t>& buffer = fbb.GetBuffer();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Lint warning: Add #include <vector> for vector<>

// If custom signals are set, inject them into the POST request body JSON
// payload.
if (!custom_signals_.empty()) {
std::map<Variant, Variant> variant_map;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Lint warning: Add #include <map> for map<>

if (last_brace != std::string::npos) {
size_t first_brace = json.find('{');
std::string insertion;
if (first_brace != std::string::npos &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Lint warning: Add #include <string> for string

}

void SetCustomSignals(std::map<std::string, std::string> custom_signals) {
custom_signals_ = std::move(custom_signals);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Lint warning: Add #include <utility> for move

void UpdatePostFields() override;

private:
std::map<std::string, std::string> custom_signals_;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Lint warning: Add #include <map> for map<>

void UpdatePostFields() override;

private:
std::map<std::string, std::string> custom_signals_;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Lint warning: Add #include <string> for string

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for setting Custom Signals in Firebase Remote Config across Android, iOS, and Desktop platforms, along with corresponding tests. The review feedback highlights three critical issues regarding type handling: on Android and iOS, passing a null Variant fails to clear the custom signal and instead logs an error or gets omitted; on Desktop, converting numeric custom signals to string representations breaks backend targeting rules that expect numeric types, so the original Variant types should be preserved.

Comment on lines +406 to +429
if (kv.second.is_string()) {
jstring str_val = env->NewStringUTF(kv.second.string_value());
result_builder =
env->CallObjectMethod(builder,
custom_signals_builder::GetMethodId(
custom_signals_builder::kPutString),
key, str_val);
env->DeleteLocalRef(str_val);
} else if (kv.second.is_int64()) {
result_builder = env->CallObjectMethod(
builder,
custom_signals_builder::GetMethodId(custom_signals_builder::kPutLong),
key, kv.second.int64_value());
} else if (kv.second.is_double()) {
result_builder =
env->CallObjectMethod(builder,
custom_signals_builder::GetMethodId(
custom_signals_builder::kPutDouble),
key, kv.second.double_value());
} else {
LogError(
"Remote Config: Invalid Variant type for SetCustomSignals() key %s.",
kv.first.c_str());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

When a null Variant is passed to SetCustomSignals, it is intended to remove/clear that custom signal (as documented in remote_config.h). However, the current implementation treats null Variants as invalid types, logs an error, and fails to pass them to the CustomSignals.Builder. This prevents developers from clearing custom signals on Android.

To fix this, explicitly handle kv.second.is_null() by calling the builder's put method with a nullptr value.

    if (kv.second.is_string()) {
      jstring str_val = env->NewStringUTF(kv.second.string_value());
      result_builder =
          env->CallObjectMethod(builder,
                                custom_signals_builder::GetMethodId(
                                    custom_signals_builder::kPutString),
                                key, str_val);
      env->DeleteLocalRef(str_val);
    } else if (kv.second.is_int64()) {
      result_builder = env->CallObjectMethod(
          builder,
          custom_signals_builder::GetMethodId(custom_signals_builder::kPutLong),
          key, kv.second.int64_value());
    } else if (kv.second.is_double()) {
      result_builder =
          env->CallObjectMethod(builder,
                                custom_signals_builder::GetMethodId(
                                    custom_signals_builder::kPutDouble),
                                key, kv.second.double_value());
    } else if (kv.second.is_null()) {
      result_builder =
          env->CallObjectMethod(builder,
                                custom_signals_builder::GetMethodId(
                                    custom_signals_builder::kPutString),
                                key, nullptr);
    } else {
      LogError(
          "Remote Config: Invalid Variant type for SetCustomSignals() key %s.",
          kv.first.c_str());
    }

Comment on lines +408 to +416
for (const auto& pair : custom_signals) {
const char* key = pair.first.c_str();
id value = VariantToNSObject(pair.second);
if (value) {
dict[@(key)] = value;
} else {
LogError("Remote Config: Invalid Variant type for SetCustomSignals() key %s", key);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Similar to the Android implementation, passing a null Variant to clear a custom signal on iOS is currently broken. VariantToNSObject returns nil for null Variants, which triggers the LogError branch and omits the key from the dictionary. Since custom signals are merged, omitting the key keeps the previous value instead of clearing it.

To fix this, check if the Variant is null and explicitly use [NSNull null] as the dictionary value, which is the standard way to represent null values in NSDictionary and is correctly interpreted by the iOS SDK to clear the signal.

  for (const auto& pair : custom_signals) {
    const char* key = pair.first.c_str();
    id value = nil;
    if (pair.second.is_null()) {
      value = [NSNull null];
    } else {
      value = VariantToNSObject(pair.second);
    }
    if (value) {
      dict[@(key)] = value;
    } else {
      LogError("Remote Config: Invalid Variant type for SetCustomSignals() key %s", key);
    }
  }

Comment on lines +275 to +294
// Merge incoming signals with existing metadata:
// - Null variants remove the existing signal entry.
// - Numeric and string types are converted and stored as string
// representations.
for (const auto& kv : custom_signals) {
const std::string& key = kv.first;
const Variant& value = kv.second;

if (value.is_null()) {
updated_signals.erase(key);
} else if (value.is_string()) {
updated_signals[key] = value.string_value();
} else if (value.is_int64()) {
updated_signals[key] = std::to_string(value.int64_value());
} else if (value.is_double()) {
std::ostringstream ss;
ss << value.double_value();
updated_signals[key] = ss.str();
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Converting all custom signal values (integers, doubles) to string representations on Desktop is a correctness issue. The Firebase Remote Config backend targeting rules distinguish between types (e.g., numeric comparisons like test_int > 40 will fail if the value is sent as a string "42"). Since Android and iOS preserve the original types (using long/double and NSNumber respectively), Desktop should also preserve these types in the REST payload.

Consider changing MetaCustomSignalsMap to store Variant instead of std::string, and serialize/deserialize them preserving their types in metadata.cc using flexbuffers, so they can be sent with their correct types in RemoteConfigRequest::UpdatePostFields.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant