From 6cd292c52e08f9a88ef85591bf39094f13e2c87e Mon Sep 17 00:00:00 2001 From: Terry Mancey Date: Fri, 24 Apr 2026 17:26:09 -0500 Subject: [PATCH] [ads] Add [virtual]:feature= condition matcher virtual pref (#35896) Adds `[virtual]:feature=|is_overridden` and `[virtual]:feature=|params|` virtual pref paths so campaign authors can check whether a named feature flag has been explicitly overridden via Griffin, a command-line switch, or a compile-time default, and inspect its field trial parameter values. Any feature name is supported without a hardcoded list. --- components/brave_ads/GLOSSARY.md | 1 + components/brave_ads/core/internal/BUILD.gn | 2 + .../core/internal/prefs/pref_util.cc | 5 + .../condition_matcher_util.h | 32 ++++++ ...tion_matcher_feature_pref_util_internal.cc | 62 +++++++++++ ...ition_matcher_feature_pref_util_internal.h | 37 +++++++ ...her_feature_pref_util_internal_unittest.cc | 101 ++++++++++++++++++ components/brave_ads/core/test/BUILD.gn | 1 + 8 files changed, 241 insertions(+) create mode 100644 components/brave_ads/core/internal/serving/targeting/condition_matcher/prefs/internal/condition_matcher_feature_pref_util_internal.cc create mode 100644 components/brave_ads/core/internal/serving/targeting/condition_matcher/prefs/internal/condition_matcher_feature_pref_util_internal.h create mode 100644 components/brave_ads/core/internal/serving/targeting/condition_matcher/prefs/internal/condition_matcher_feature_pref_util_internal_unittest.cc diff --git a/components/brave_ads/GLOSSARY.md b/components/brave_ads/GLOSSARY.md index d12d339313a..e2beb8c5d12 100644 --- a/components/brave_ads/GLOSSARY.md +++ b/components/brave_ads/GLOSSARY.md @@ -20,6 +20,7 @@ A place to define all specific terms and vocabulary for the Brave Ads component, | Catalog | A collection of available campaigns, creative sets and creative instances. | | Click | Refers to a user interacting with an advertisement by clicking on the ad. | | Click-through rate | The percentage of ad impressions that result in clicks. | +| Condition matcher | One or more pref path and condition pairs, evaluated with AND logic on the user's device, that must all match before a new tab page ad can be served. Paths may reference stored prefs or virtual prefs computed at runtime, and conditions may use epoch, numerical, regex, or pattern operators. | | Confirmations | Confirm events, i.e., views, without revealing to Brave the particular user involved. See [security and privacy model for ad confirmations](https://github.com/brave/brave-browser/wiki/Security-and-privacy-model-for-ad-confirmations). | | Contextual | Contextual advertising targets ads based on the web page's content or the user's online activity context to deliver relevant and personalized advertisements. | | Conversion | When a user triggers an action, it is counted as a conversion. Conversions include making a purchase or signing up for a newsletter. | diff --git a/components/brave_ads/core/internal/BUILD.gn b/components/brave_ads/core/internal/BUILD.gn index be8387dc39b..d846f5bb1ab 100644 --- a/components/brave_ads/core/internal/BUILD.gn +++ b/components/brave_ads/core/internal/BUILD.gn @@ -874,6 +874,8 @@ static_library("internal") { "serving/targeting/condition_matcher/matchers/regex_condition_matcher_util.h", "serving/targeting/condition_matcher/prefs/condition_matcher_pref_util.cc", "serving/targeting/condition_matcher/prefs/condition_matcher_pref_util.h", + "serving/targeting/condition_matcher/prefs/internal/condition_matcher_feature_pref_util_internal.cc", + "serving/targeting/condition_matcher/prefs/internal/condition_matcher_feature_pref_util_internal.h", "serving/targeting/condition_matcher/prefs/internal/condition_matcher_keyword_path_component_util.cc", "serving/targeting/condition_matcher/prefs/internal/condition_matcher_keyword_path_component_util.h", "serving/targeting/condition_matcher/prefs/internal/condition_matcher_pref_util_internal.cc", diff --git a/components/brave_ads/core/internal/prefs/pref_util.cc b/components/brave_ads/core/internal/prefs/pref_util.cc index dba70db25ef..d69560653de 100644 --- a/components/brave_ads/core/internal/prefs/pref_util.cc +++ b/components/brave_ads/core/internal/prefs/pref_util.cc @@ -13,6 +13,7 @@ #include "base/strings/string_number_conversions.h" #include "base/time/time.h" #include "brave/components/brave_ads/core/internal/ads_client/ads_client_util.h" +#include "brave/components/brave_ads/core/internal/serving/targeting/condition_matcher/prefs/internal/condition_matcher_feature_pref_util_internal.h" #include "brave/components/brave_ads/core/public/ads_client/ads_client.h" namespace brave_ads { @@ -390,6 +391,10 @@ bool HasLocalStatePrefPath(const std::string& path) { std::optional GetVirtualPref(const base::DictValue& virtual_prefs, std::string_view path) { if (path.starts_with(kVirtualPrefPathPrefix)) { + if (std::optional dict = MaybeGetFeaturePrefValue(path)) { + return dict; + } + if (const base::Value* const value = virtual_prefs.Find(path)) { return value->Clone(); } diff --git a/components/brave_ads/core/internal/serving/targeting/condition_matcher/condition_matcher_util.h b/components/brave_ads/core/internal/serving/targeting/condition_matcher/condition_matcher_util.h index 63fff8b535c..cc5dfdc5288 100644 --- a/components/brave_ads/core/internal/serving/targeting/condition_matcher/condition_matcher_util.h +++ b/components/brave_ads/core/internal/serving/targeting/condition_matcher/condition_matcher_util.h @@ -239,6 +239,38 @@ class DictValue; // } // ] // +// 9. "[virtual]:feature=|is_overridden" +// - Returns "1" if the named feature flag has been explicitly overridden +// (via Griffin, a command-line switch, or a compile-time default +// override), or "0" otherwise. Any feature name is supported, including +// future ones not yet known at build time. For example, the following +// condition matcher will match when `NotificationAdFeature` is +// overridden: +// +// "conditionMatchers": [ +// { +// "condition": "1", +// "prefPath": +// "[virtual]:feature=NotificationAdFeature|is_overridden" +// } +// ] +// +// 10. "[virtual]:feature=|params|" +// - Returns the string value of the named field trial param for the given +// feature, or does not match if the feature has no associated trial or +// the param does not exist. Combine with numerical operators to compare +// numeric param values. For example, the following condition matcher +// will match when the `version` param for `NotificationAdServing` +// equals 2: +// +// "conditionMatchers": [ +// { +// "condition": "[R=]:2", +// "prefPath": +// "[virtual]:feature=NotificationAdServing|params|version" +// } +// ] +// // NOTE: To identify condition matchers, first create a copy of your // brave://local-state and `Default/Preferences` files. Next, change a // brave://setting or enable a feature, quit the browser and then compare the diff --git a/components/brave_ads/core/internal/serving/targeting/condition_matcher/prefs/internal/condition_matcher_feature_pref_util_internal.cc b/components/brave_ads/core/internal/serving/targeting/condition_matcher/prefs/internal/condition_matcher_feature_pref_util_internal.cc new file mode 100644 index 00000000000..9ad591bf83d --- /dev/null +++ b/components/brave_ads/core/internal/serving/targeting/condition_matcher/prefs/internal/condition_matcher_feature_pref_util_internal.cc @@ -0,0 +1,62 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_ads/core/internal/serving/targeting/condition_matcher/prefs/internal/condition_matcher_feature_pref_util_internal.h" + +#include + +#include "base/feature_list.h" +#include "base/metrics/field_trial.h" +#include "base/metrics/field_trial_params.h" +#include "base/strings/string_number_conversions.h" +#include "base/values.h" +#include "brave/components/brave_ads/core/internal/serving/targeting/condition_matcher/prefs/internal/condition_matcher_keyword_path_component_util.h" + +namespace brave_ads { + +namespace { + +constexpr std::string_view kIsOverriddenKey = "is_overridden"; +constexpr std::string_view kParamsKey = "params"; + +} // namespace + +std::optional MaybeGetFeaturePrefValue( + std::string_view path_component) { + std::optional feature_name = + MaybeParseKeywordPathComponentValue(path_component, + kFeatureVirtualPrefKeyword); + if (!feature_name || feature_name->empty()) { + return std::nullopt; + } + + base::FeatureList* const feature_list = base::FeatureList::GetInstance(); + + const bool is_overridden = feature_list != nullptr && + feature_list->IsFeatureOverridden(*feature_name); + + base::DictValue dict = base::DictValue().Set( + kIsOverriddenKey, base::NumberToString(static_cast(is_overridden))); + + if (feature_list) { + const base::FieldTrial* const field_trial = + feature_list->GetAssociatedFieldTrialByFeatureName(*feature_name); + if (field_trial != nullptr) { + base::FieldTrialParams field_trial_params; + if (base::GetFieldTrialParams(field_trial->trial_name(), + &field_trial_params)) { + base::DictValue params_dict; + for (const auto& [param_name, param_value] : field_trial_params) { + params_dict.Set(param_name, param_value); + } + dict.Set(kParamsKey, std::move(params_dict)); + } + } + } + + return base::Value(std::move(dict)); +} + +} // namespace brave_ads diff --git a/components/brave_ads/core/internal/serving/targeting/condition_matcher/prefs/internal/condition_matcher_feature_pref_util_internal.h b/components/brave_ads/core/internal/serving/targeting/condition_matcher/prefs/internal/condition_matcher_feature_pref_util_internal.h new file mode 100644 index 00000000000..7d266485f6c --- /dev/null +++ b/components/brave_ads/core/internal/serving/targeting/condition_matcher/prefs/internal/condition_matcher_feature_pref_util_internal.h @@ -0,0 +1,37 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_COMPONENTS_BRAVE_ADS_CORE_INTERNAL_SERVING_TARGETING_CONDITION_MATCHER_PREFS_INTERNAL_CONDITION_MATCHER_FEATURE_PREF_UTIL_INTERNAL_H_ +#define BRAVE_COMPONENTS_BRAVE_ADS_CORE_INTERNAL_SERVING_TARGETING_CONDITION_MATCHER_PREFS_INTERNAL_CONDITION_MATCHER_FEATURE_PREF_UTIL_INTERNAL_H_ + +#include +#include + +namespace base { +class Value; +} // namespace base + +// Resolves `[virtual]:feature=` keyword path components into a +// dict whose sub-paths are traversed via the normal `|` separator. Any feature +// name is supported, including future ones not yet known at build time. + +namespace brave_ads { + +inline constexpr std::string_view kFeatureVirtualPrefKeyword = + "[virtual]:feature"; + +// Returns a dict with an `is_overridden` string, either "1" if overridden or +// "0" if not; unknown feature names are treated as not overridden. When a +// field trial is associated the dict also contains a `params` sub-dict of +// param name-value pairs; otherwise `params` is absent and any +// `|params|` traversal returns nothing. Returns `std::nullopt` if +// `path_component` does not match `[virtual]:feature=` or the +// feature name is empty. +std::optional MaybeGetFeaturePrefValue( + std::string_view path_component); + +} // namespace brave_ads + +#endif // BRAVE_COMPONENTS_BRAVE_ADS_CORE_INTERNAL_SERVING_TARGETING_CONDITION_MATCHER_PREFS_INTERNAL_CONDITION_MATCHER_FEATURE_PREF_UTIL_INTERNAL_H_ diff --git a/components/brave_ads/core/internal/serving/targeting/condition_matcher/prefs/internal/condition_matcher_feature_pref_util_internal_unittest.cc b/components/brave_ads/core/internal/serving/targeting/condition_matcher/prefs/internal/condition_matcher_feature_pref_util_internal_unittest.cc new file mode 100644 index 00000000000..fe03d6de1bc --- /dev/null +++ b/components/brave_ads/core/internal/serving/targeting/condition_matcher/prefs/internal/condition_matcher_feature_pref_util_internal_unittest.cc @@ -0,0 +1,101 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_ads/core/internal/serving/targeting/condition_matcher/prefs/internal/condition_matcher_feature_pref_util_internal.h" + +#include "base/feature_list.h" +#include "base/test/scoped_feature_list.h" +#include "base/values.h" +#include "brave/components/brave_ads/core/internal/common/test/test_base.h" + +// npm run test -- brave_all_unit_tests --filter=BraveAds* + +namespace brave_ads { + +namespace { +BASE_FEATURE(kTestFeature, base::FEATURE_ENABLED_BY_DEFAULT); +} // namespace + +class BraveAdsConditionMatcherFeaturePrefUtilInternalTest + : public test::TestBase {}; + +TEST_F(BraveAdsConditionMatcherFeaturePrefUtilInternalTest, + GetIsOverriddenWhenFeatureIsOverridden) { + // Arrange + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndEnableFeature(kTestFeature); + + // Act + std::optional feature_dict = + MaybeGetFeaturePrefValue("[virtual]:feature=TestFeature"); + + // Assert + ASSERT_TRUE(feature_dict); + EXPECT_EQ("1", *feature_dict->GetDict().FindString("is_overridden")); +} + +TEST_F(BraveAdsConditionMatcherFeaturePrefUtilInternalTest, + GetIsNotOverriddenWhenFeatureIsNotOverridden) { + // Act + std::optional feature_dict = + MaybeGetFeaturePrefValue("[virtual]:feature=TestFeature"); + + // Assert + ASSERT_TRUE(feature_dict); + EXPECT_EQ("0", *feature_dict->GetDict().FindString("is_overridden")); +} + +TEST_F(BraveAdsConditionMatcherFeaturePrefUtilInternalTest, + GetParamsWhenFeatureHasTrial) { + // Arrange + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndEnableFeatureWithParameters(kTestFeature, + {{"foo", "bar"}}); + + // Act + std::optional feature_dict = + MaybeGetFeaturePrefValue("[virtual]:feature=TestFeature"); + + // Assert + ASSERT_TRUE(feature_dict); + const auto* const params_dict = feature_dict->GetDict().FindDict("params"); + ASSERT_TRUE(params_dict); + EXPECT_EQ("bar", *params_dict->FindString("foo")); +} + +TEST_F(BraveAdsConditionMatcherFeaturePrefUtilInternalTest, + DoNotGetParamsDictWhenFeatureHasNoTrial) { + // Arrange + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndEnableFeature(kTestFeature); + + // Act + std::optional feature_dict = + MaybeGetFeaturePrefValue("[virtual]:feature=TestFeature"); + + // Assert + ASSERT_TRUE(feature_dict); + EXPECT_FALSE(feature_dict->GetDict().FindDict("params")); +} + +TEST_F(BraveAdsConditionMatcherFeaturePrefUtilInternalTest, + DoNotGetFeaturePrefValueForNonKeywordPath) { + // Act & Assert + EXPECT_FALSE(MaybeGetFeaturePrefValue("TestFeature")); +} + +TEST_F(BraveAdsConditionMatcherFeaturePrefUtilInternalTest, + DoNotGetFeaturePrefValueForEmptyFeatureName) { + // Act & Assert + EXPECT_FALSE(MaybeGetFeaturePrefValue("[virtual]:feature=")); +} + +TEST_F(BraveAdsConditionMatcherFeaturePrefUtilInternalTest, + DoNotGetFeaturePrefValueForKeywordWithNoEqualsSign) { + // Act & Assert + EXPECT_FALSE(MaybeGetFeaturePrefValue("[virtual]:feature")); +} + +} // namespace brave_ads diff --git a/components/brave_ads/core/test/BUILD.gn b/components/brave_ads/core/test/BUILD.gn index 4d28927e040..121c58190b4 100644 --- a/components/brave_ads/core/test/BUILD.gn +++ b/components/brave_ads/core/test/BUILD.gn @@ -530,6 +530,7 @@ source_set("brave_ads_unit_tests") { "//brave/components/brave_ads/core/internal/serving/targeting/condition_matcher/matchers/pattern_condition_matcher_util_unittest.cc", "//brave/components/brave_ads/core/internal/serving/targeting/condition_matcher/matchers/regex_condition_matcher_util_unittest.cc", "//brave/components/brave_ads/core/internal/serving/targeting/condition_matcher/prefs/condition_matcher_pref_util_unittest.cc", + "//brave/components/brave_ads/core/internal/serving/targeting/condition_matcher/prefs/internal/condition_matcher_feature_pref_util_internal_unittest.cc", "//brave/components/brave_ads/core/internal/serving/targeting/condition_matcher/prefs/internal/condition_matcher_keyword_path_component_util_unittest.cc", "//brave/components/brave_ads/core/internal/serving/targeting/condition_matcher/prefs/internal/condition_matcher_pref_util_internal_unittest.cc", "//brave/components/brave_ads/core/internal/serving/targeting/condition_matcher/prefs/internal/condition_matcher_time_period_storage_pref_util_internal_unittest.cc",