Clickiness: Wire response from URLLoader to DB, add e2e tests
Checking if origins are allowed (allowed by user settings, attested, etc.) will come in a subsequent CL. Doesn't include the redirection logic -- that will land in crrev.com/c/6353624. Bug: 394108643 Change-Id: Ic3897262c17afa7a7cbae967b9f266277ea77a80 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6336937 Auto-Submit: Caleb Raitto <caraitto@chromium.org> Commit-Queue: Yuwei Huang <yuweih@chromium.org> Reviewed-by: Maks Orlovich <morlovich@chromium.org> Reviewed-by: Kenichi Ishibashi <bashi@chromium.org> Reviewed-by: James Cook <jamescook@chromium.org> Reviewed-by: Yuwei Huang <yuweih@chromium.org> Reviewed-by: Austin Sullivan <asully@chromium.org> Reviewed-by: Ken Buchanan <kenrb@chromium.org> Cr-Commit-Position: refs/heads/main@{#1432935}
This commit is contained in:

committed by
Chromium LUCI CQ

parent
b07519b2f7
commit
c321aa7496
chrome/browser/ash/bruschetta
content/browser
interest_group
interest_group_browsertest.ccinterest_group_caching_storage.ccinterest_group_caching_storage.hinterest_group_manager_impl.ccinterest_group_manager_impl.h
network_service_client.ccnetwork_service_client.hstorage_partition_impl.ccstorage_partition_impl.hremoting/base
services/network
testing/variations
third_party/blink/renderer/modules/ad_auction
@ -268,6 +268,9 @@ void BruschettaNetworkContext::OnSharedStorageHeaderReceived(
|
||||
std::move(callback).Run();
|
||||
}
|
||||
|
||||
void BruschettaNetworkContext::OnAdAuctionEventRecordHeaderReceived(
|
||||
network::AdAuctionEventRecord event_record) {}
|
||||
|
||||
void BruschettaNetworkContext::Clone(
|
||||
mojo::PendingReceiver<network::mojom::URLLoaderNetworkServiceObserver>
|
||||
observer) {
|
||||
|
@ -92,6 +92,8 @@ class BruschettaNetworkContext
|
||||
methods_with_options,
|
||||
const std::optional<std::string>& with_lock,
|
||||
OnSharedStorageHeaderReceivedCallback callback) override;
|
||||
void OnAdAuctionEventRecordHeaderReceived(
|
||||
network::AdAuctionEventRecord event_record) override;
|
||||
void Clone(
|
||||
mojo::PendingReceiver<network::mojom::URLLoaderNetworkServiceObserver>
|
||||
listener) override;
|
||||
|
@ -154,6 +154,12 @@ constexpr char kLegitimateAdAuctionSignals[] =
|
||||
// throwing an exception.
|
||||
const char kSuccess[] = "success";
|
||||
|
||||
// A path for an update registered by `RegisterNoOpUpdate()` that returns an
|
||||
// empty dict, which is a valid update, but doesn't change the interest group.
|
||||
// Can be used with
|
||||
// WaitForInterestGroupsSatisfyingInvalidatingCacheByUpdating().
|
||||
constexpr char kNoOpUpdatePath[] = "/interest_group/no_op_update_path.json";
|
||||
|
||||
// Returns a string that declares a "maybePromise()" Javascript function, which
|
||||
// takes an argument and either returns it (if `use_promise` is false) or
|
||||
// returns a promise that will be resolved with that value in a millisecond (if
|
||||
@ -458,12 +464,14 @@ class NetworkResponder {
|
||||
void RegisterNetworkResponse(const std::string& url_path,
|
||||
std::string_view body,
|
||||
std::string_view mime_type = "application/json",
|
||||
ResponseHeaders extra_response_headers = {}) {
|
||||
ResponseHeaders extra_response_headers = {},
|
||||
net::HttpStatusCode code = net::HTTP_OK) {
|
||||
base::AutoLock auto_lock(response_map_lock_);
|
||||
Response response;
|
||||
response.body = body;
|
||||
response.mime_type = mime_type;
|
||||
response.extra_response_headers = std::move(extra_response_headers);
|
||||
response.code = code;
|
||||
response_map_[url_path] = std::move(response);
|
||||
}
|
||||
|
||||
@ -621,6 +629,7 @@ function generateBid(
|
||||
std::string body;
|
||||
std::string mime_type;
|
||||
ResponseHeaders extra_response_headers;
|
||||
net::HttpStatusCode code;
|
||||
};
|
||||
|
||||
std::unique_ptr<net::test_server::HttpResponse> RequestHandler(
|
||||
@ -632,7 +641,7 @@ function generateBid(
|
||||
}
|
||||
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
|
||||
response->AddCustomHeader(kFledgeHeader, "true");
|
||||
response->set_code(net::HTTP_OK);
|
||||
response->set_code(it->second.code);
|
||||
response->set_content(it->second.body);
|
||||
response->set_content_type(it->second.mime_type);
|
||||
for (const auto& header : it->second.extra_response_headers) {
|
||||
@ -767,7 +776,9 @@ class InterestGroupBrowserTest : public ContentBrowserTest {
|
||||
{blink::features::kFledgeDirectFromSellerSignalsWebBundles, {}},
|
||||
{blink::features::kFledgeTrustedSignalsKVv2Support, {}},
|
||||
{blink::features::kFledgeTrustedSignalsKVv1CreativeScanning, {}},
|
||||
{features::kFledgeTextConversionHelpers, {}}},
|
||||
{features::kFledgeTextConversionHelpers, {}},
|
||||
{network::features::kAdAuctionEventRegistration, {}},
|
||||
{blink::features::kFledgeClickiness, {}}},
|
||||
/*disabled_features=*/
|
||||
{blink::features::kFencedFrames,
|
||||
blink::features::kFledgeEnforceKAnonymity,
|
||||
@ -969,6 +980,14 @@ class InterestGroupBrowserTest : public ContentBrowserTest {
|
||||
dict.Set("trustedBiddingSignalsCoordinator",
|
||||
group.trusted_bidding_signals_coordinator->Serialize());
|
||||
}
|
||||
if (group.view_and_click_counts_providers) {
|
||||
base::Value::List providers;
|
||||
for (const url::Origin& provider :
|
||||
*group.view_and_click_counts_providers) {
|
||||
providers.Append(provider.Serialize());
|
||||
}
|
||||
dict.Set("viewAndClickCountsProviders", std::move(providers));
|
||||
}
|
||||
if (group.user_bidding_signals) {
|
||||
dict.Set("userBiddingSignals", JsonToValue(*group.user_bidding_signals));
|
||||
}
|
||||
@ -1517,6 +1536,41 @@ function provideAdditionalBids(seller, nonce, bidStringList,
|
||||
}
|
||||
}
|
||||
|
||||
// Waits until the `condition` callback over the interest groups returns
|
||||
// true, running an update on the owner's groups in order to force interest
|
||||
// group cache invalidation.
|
||||
//
|
||||
// This can be useful for loading things like clickiness data, whose updates
|
||||
// intentionally don't invalidate cache.
|
||||
//
|
||||
// **REQUIREMENT**: At least one interest group owned by `owner` must have a
|
||||
// valid updateURL and response registered, but the update response can be an
|
||||
// empty dict. RegisterNoOpUpdate() and kNoOpUpdatePath can be used for this.
|
||||
//
|
||||
// Also, the current top-level page origin should be the same as `owner`.
|
||||
void WaitForInterestGroupsSatisfyingInvalidatingCacheByUpdating(
|
||||
const url::Origin& owner,
|
||||
base::RepeatingCallback<bool(scoped_refptr<StorageInterestGroups>)>
|
||||
condition) {
|
||||
ASSERT_EQ(owner, shell()
|
||||
->web_contents()
|
||||
->GetPrimaryMainFrame()
|
||||
->GetLastCommittedOrigin());
|
||||
while (true) {
|
||||
EXPECT_EQ("done", UpdateInterestGroupsInJS());
|
||||
if (condition.Run(GetInterestGroupsForOwner(owner))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Registers an update at kNoOpUpdatePath that returns an empty dict, which
|
||||
// is a valid update, but doesn't change the interest group. Can be used with
|
||||
// WaitForInterestGroupsSatisfyingInvalidatingCacheByUpdating().
|
||||
void RegisterNoOpUpdate() {
|
||||
network_responder_->RegisterNetworkResponse(kNoOpUpdatePath, "{}");
|
||||
}
|
||||
|
||||
// Waits for `url` to be requested by `embedded_https_test_server()`, or any
|
||||
// other server that OnHttpsTestServerRequestMonitor() has been configured to
|
||||
// monitor. `url`'s hostname is replaced with "127.0.0.1", since the embedded
|
||||
@ -7891,6 +7945,219 @@ IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
|
||||
EXPECT_TRUE(console_observer.Wait());
|
||||
}
|
||||
|
||||
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, Clickiness_CaptureView) {
|
||||
constexpr char kRecordViewClickPath[] =
|
||||
"/interest_group/record_view_click_event.html";
|
||||
|
||||
GURL test_url_a = embedded_https_test_server().GetURL(
|
||||
"a.test", "/attribution_reporting/page_with_impression_creator.html");
|
||||
url::Origin test_origin_a = url::Origin::Create(test_url_a);
|
||||
ASSERT_TRUE(test_url_a.SchemeIs(url::kHttpsScheme));
|
||||
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
|
||||
|
||||
const std::string record_event_response = base::StringPrintf(
|
||||
"type=\"view\", eligible-origins=(\"%s\")", test_origin_a.Serialize());
|
||||
|
||||
network_responder_->RegisterNetworkResponse(
|
||||
kRecordViewClickPath, "Throwaway response", "image/jpeg",
|
||||
/*extra_response_headers=*/
|
||||
{{"Ad-Auction-Record-Event", record_event_response}});
|
||||
|
||||
GURL record_event_url =
|
||||
embedded_https_test_server().GetURL("c.test", kRecordViewClickPath);
|
||||
|
||||
EXPECT_TRUE(ExecJs(web_contents(), JsReplace("createAttributionSrcImg($1);",
|
||||
record_event_url)));
|
||||
|
||||
// This join should succeed. Register a no-op update URL to use
|
||||
// WaitForInterestGroupsSatisfyingInvalidatingCacheByUpdating().
|
||||
RegisterNoOpUpdate();
|
||||
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
|
||||
blink::TestInterestGroupBuilder(test_origin_a, "cars")
|
||||
.SetViewAndClickCountsProviders(
|
||||
{{url::Origin::Create(record_event_url)}})
|
||||
.SetUpdateUrl(embedded_https_test_server().GetURL(
|
||||
"a.test", kNoOpUpdatePath))
|
||||
.Build()));
|
||||
|
||||
WaitForInterestGroupsSatisfyingInvalidatingCacheByUpdating(
|
||||
test_origin_a,
|
||||
base::BindLambdaForTesting(
|
||||
[](scoped_refptr<StorageInterestGroups> groups) {
|
||||
EXPECT_EQ(groups->size(), 1u);
|
||||
const StorageInterestGroup& group = *groups->GetInterestGroups()[0];
|
||||
const blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
|
||||
group.bidding_browser_signals->view_and_click_counts;
|
||||
EXPECT_EQ(group.interest_group.name, "cars");
|
||||
return view_and_click_counts->view_counts->past_hour == 1 &&
|
||||
view_and_click_counts->click_counts->past_hour == 0;
|
||||
}));
|
||||
|
||||
// TODO(crbug.com/394108643): Also check generateBid() once the plumbing is
|
||||
// hooked up.
|
||||
}
|
||||
|
||||
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, Clickiness_CaptureClick) {
|
||||
constexpr char kRecordViewClickPath[] =
|
||||
"/interest_group/record_view_click_event.html";
|
||||
|
||||
GURL test_url_a = embedded_https_test_server().GetURL(
|
||||
"a.test", "/attribution_reporting/page_with_impression_creator.html");
|
||||
url::Origin test_origin_a = url::Origin::Create(test_url_a);
|
||||
ASSERT_TRUE(test_url_a.SchemeIs(url::kHttpsScheme));
|
||||
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
|
||||
|
||||
const std::string record_event_response = base::StringPrintf(
|
||||
"type=\"click\", eligible-origins=(\"%s\")", test_origin_a.Serialize());
|
||||
|
||||
network_responder_->RegisterNetworkResponse(
|
||||
kRecordViewClickPath, "Throwaway response", "image/jpeg",
|
||||
/*extra_response_headers=*/
|
||||
{{"Ad-Auction-Record-Event", record_event_response}});
|
||||
|
||||
GURL record_event_url =
|
||||
embedded_https_test_server().GetURL("c.test", kRecordViewClickPath);
|
||||
|
||||
EXPECT_TRUE(
|
||||
ExecJs(web_contents(),
|
||||
JsReplace(R"(
|
||||
createAttributionSrcAnchor({id: 'link',
|
||||
url: $1,
|
||||
attributionsrc: $2,
|
||||
target: $3});)",
|
||||
embedded_https_test_server().GetURL("a.test", "/echo"),
|
||||
record_event_url, "_top")));
|
||||
TestNavigationObserver observer(web_contents());
|
||||
EXPECT_TRUE(ExecJs(web_contents(), "simulateClick('link');"));
|
||||
observer.Wait();
|
||||
|
||||
// This join should succeed. Register a no-op update URL to use
|
||||
// WaitForInterestGroupsSatisfyingInvalidatingCacheByUpdating().
|
||||
RegisterNoOpUpdate();
|
||||
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
|
||||
blink::TestInterestGroupBuilder(test_origin_a, "cars")
|
||||
.SetViewAndClickCountsProviders(
|
||||
{{url::Origin::Create(record_event_url)}})
|
||||
.SetUpdateUrl(embedded_https_test_server().GetURL(
|
||||
"a.test", kNoOpUpdatePath))
|
||||
.Build()));
|
||||
|
||||
WaitForInterestGroupsSatisfyingInvalidatingCacheByUpdating(
|
||||
test_origin_a,
|
||||
base::BindLambdaForTesting(
|
||||
[](scoped_refptr<StorageInterestGroups> groups) {
|
||||
EXPECT_EQ(groups->size(), 1u);
|
||||
const StorageInterestGroup& group = *groups->GetInterestGroups()[0];
|
||||
const blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
|
||||
group.bidding_browser_signals->view_and_click_counts;
|
||||
EXPECT_EQ(group.interest_group.name, "cars");
|
||||
return view_and_click_counts->view_counts->past_hour == 0 &&
|
||||
view_and_click_counts->click_counts->past_hour == 1;
|
||||
}));
|
||||
|
||||
// TODO(crbug.com/394108643): Also check generateBid() once the plumbing is
|
||||
// hooked up.
|
||||
}
|
||||
|
||||
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, Clickiness_NotStructuredDict) {
|
||||
constexpr char kRecordViewClickPath[] =
|
||||
"/interest_group/record_view_click_event.html";
|
||||
|
||||
GURL test_url_a = embedded_https_test_server().GetURL(
|
||||
"a.test", "/attribution_reporting/page_with_impression_creator.html");
|
||||
url::Origin test_origin_a = url::Origin::Create(test_url_a);
|
||||
ASSERT_TRUE(test_url_a.SchemeIs(url::kHttpsScheme));
|
||||
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
|
||||
|
||||
// Double equals isn't valid in structured headers -- this message should be
|
||||
// rejected.
|
||||
const std::string record_event_response = base::StringPrintf(
|
||||
"type==\"view\", eligible-origins=(\"%s\")", test_origin_a.Serialize());
|
||||
|
||||
network_responder_->RegisterNetworkResponse(
|
||||
kRecordViewClickPath, "Throwaway response", "image/jpeg",
|
||||
/*extra_response_headers=*/
|
||||
{{"Ad-Auction-Record-Event", record_event_response}});
|
||||
|
||||
GURL record_event_url =
|
||||
embedded_https_test_server().GetURL("c.test", kRecordViewClickPath);
|
||||
|
||||
EXPECT_TRUE(ExecJs(web_contents(), JsReplace("createAttributionSrcImg($1);",
|
||||
record_event_url)));
|
||||
|
||||
// This join should succeed.
|
||||
base::RunLoop().RunUntilIdle();
|
||||
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
|
||||
blink::TestInterestGroupBuilder(test_origin_a, "cars")
|
||||
.SetViewAndClickCountsProviders(
|
||||
{{url::Origin::Create(record_event_url)}})
|
||||
.Build()));
|
||||
|
||||
// No change to view or click counts. Note that this is somewhat racy, as if
|
||||
// the product code erroneously records a view or a click, it could happen
|
||||
// after the join -- in that case, failures may be flaky. However, correct
|
||||
// product code shouldn't result in flakes.
|
||||
scoped_refptr<StorageInterestGroups> groups =
|
||||
GetInterestGroupsForOwner(test_origin_a);
|
||||
ASSERT_EQ(groups->size(), 1u);
|
||||
const StorageInterestGroup& group = *groups->GetInterestGroups()[0];
|
||||
const blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
|
||||
group.bidding_browser_signals->view_and_click_counts;
|
||||
EXPECT_EQ(group.interest_group.name, "cars");
|
||||
EXPECT_EQ(view_and_click_counts->view_counts->past_hour, 0);
|
||||
EXPECT_EQ(view_and_click_counts->click_counts->past_hour, 0);
|
||||
}
|
||||
|
||||
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, Clickiness_InvalidType) {
|
||||
constexpr char kRecordViewClickPath[] =
|
||||
"/interest_group/record_view_click_event.html";
|
||||
|
||||
GURL test_url_a = embedded_https_test_server().GetURL(
|
||||
"a.test", "/attribution_reporting/page_with_impression_creator.html");
|
||||
url::Origin test_origin_a = url::Origin::Create(test_url_a);
|
||||
ASSERT_TRUE(test_url_a.SchemeIs(url::kHttpsScheme));
|
||||
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
|
||||
|
||||
// A valid structured dictionary, but not-view-or-click isn't a valid type --
|
||||
// this message should be rejected.
|
||||
const std::string record_event_response = base::StringPrintf(
|
||||
"type=\"not-view-or-click\", eligible-origins=(\"%s\")",
|
||||
test_origin_a.Serialize());
|
||||
|
||||
network_responder_->RegisterNetworkResponse(
|
||||
kRecordViewClickPath, "Throwaway response", "image/jpeg",
|
||||
/*extra_response_headers=*/
|
||||
{{"Ad-Auction-Record-Event", record_event_response}});
|
||||
|
||||
GURL record_event_url =
|
||||
embedded_https_test_server().GetURL("c.test", kRecordViewClickPath);
|
||||
|
||||
EXPECT_TRUE(ExecJs(web_contents(), JsReplace("createAttributionSrcImg($1);",
|
||||
record_event_url)));
|
||||
|
||||
// This join should succeed.
|
||||
base::RunLoop().RunUntilIdle();
|
||||
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
|
||||
blink::TestInterestGroupBuilder(test_origin_a, "cars")
|
||||
.SetViewAndClickCountsProviders(
|
||||
{{url::Origin::Create(record_event_url)}})
|
||||
.Build()));
|
||||
|
||||
// No change to view or click counts. Note that this is somewhat racy, as if
|
||||
// the product code erroneously records a view or a click, it could happen
|
||||
// after the join -- in that case, failures may be flaky. However, correct
|
||||
// product code shouldn't result in flakes.
|
||||
scoped_refptr<StorageInterestGroups> groups =
|
||||
GetInterestGroupsForOwner(test_origin_a);
|
||||
ASSERT_EQ(groups->size(), 1u);
|
||||
const StorageInterestGroup& group = *groups->GetInterestGroups()[0];
|
||||
const blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
|
||||
group.bidding_browser_signals->view_and_click_counts;
|
||||
EXPECT_EQ(group.interest_group.name, "cars");
|
||||
EXPECT_EQ(view_and_click_counts->view_counts->past_hour, 0);
|
||||
EXPECT_EQ(view_and_click_counts->click_counts->past_hour, 0);
|
||||
}
|
||||
|
||||
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
|
||||
RunAdAuctionBuyersNoInterestGroup) {
|
||||
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
|
||||
|
@ -363,6 +363,23 @@ void InterestGroupCachingStorage::RecordDebugReportCooldown(
|
||||
.WithArgs(origin, cooldown_start, cooldown_type);
|
||||
}
|
||||
|
||||
void InterestGroupCachingStorage::RecordViewClick(
|
||||
network::AdAuctionEventRecord event_record) {
|
||||
// Cached interest groups containing stale view / click counts are
|
||||
// intentionally not evicted -- views especially occur frequently, and would
|
||||
// result in many evictions, limiting the usefulness of this cache. So, for
|
||||
// performance, it is better to return view / click data that's slightly
|
||||
// stale.
|
||||
//
|
||||
// TODO(crbug.com/394108643): Cap the time duration of this staleness with a
|
||||
// new timer that evicts groups loaded more than say 120 seconds ago. Without
|
||||
// this, in the rare case that auctions that each load a given IG are running
|
||||
// constantly, back-to-back, the view click data for that IG could become
|
||||
// arbitrarily stale.
|
||||
interest_group_storage_.AsyncCall(&InterestGroupStorage::RecordViewClick)
|
||||
.WithArgs(std::move(event_record));
|
||||
}
|
||||
|
||||
void InterestGroupCachingStorage::UpdateKAnonymity(
|
||||
const blink::InterestGroupKey& interest_group_key,
|
||||
const std::vector<std::string>& positive_hashed_keys,
|
||||
|
@ -225,6 +225,9 @@ class CONTENT_EXPORT InterestGroupCachingStorage {
|
||||
void RecordDebugReportCooldown(const url::Origin& origin,
|
||||
base::Time cooldown_start,
|
||||
DebugReportCooldownType cooldown_type);
|
||||
// Records a view or a click event. Aggregate time bucketed view and click
|
||||
// information is provided to bidder's browsing signals in generateBid().
|
||||
void RecordViewClick(network::AdAuctionEventRecord event_record);
|
||||
// Records a K-anonymity update for an interest group. If
|
||||
// `replace_existing_values` is true, this update will store the new
|
||||
// `update_time` and `positive_hashed_values`, replacing the interest
|
||||
|
@ -524,6 +524,19 @@ void InterestGroupManagerImpl::RecordDebugReportCooldown(
|
||||
cooldown_type);
|
||||
}
|
||||
|
||||
void InterestGroupManagerImpl::RecordViewClick(
|
||||
network::AdAuctionEventRecord event_record) {
|
||||
// TODO(crbug.com/394108643): Check against
|
||||
// ContentBrowserClient::IsInterestGroupAPIAllowed(). This will require
|
||||
// getting an RFH; we could use something like
|
||||
// url_loader_network_observers_.current_context().navigation_or_document()
|
||||
// in the StoragePartitionImpl, but the issue is that for clicks, we'll
|
||||
// probably get a navigation handle instead of an RFH? Perhaps we can
|
||||
// override WebContentsObserver::DidFinishNavigation() to wait until the new
|
||||
// RFH is ready?
|
||||
caching_storage_.RecordViewClick(std::move(event_record));
|
||||
}
|
||||
|
||||
void InterestGroupManagerImpl::RegisterAdKeysAsJoined(
|
||||
base::flat_set<std::string> hashed_keys) {
|
||||
k_anonymity_manager_->RegisterAdKeysAsJoined(std::move(hashed_keys));
|
||||
|
@ -280,6 +280,10 @@ class CONTENT_EXPORT InterestGroupManagerImpl : public InterestGroupManager {
|
||||
base::Time cooldown_start,
|
||||
DebugReportCooldownType cooldown_type);
|
||||
|
||||
// Records a view or a click event. Aggregate time bucketed view and click
|
||||
// information is provided to bidder's browsing signals in generateBid().
|
||||
void RecordViewClick(network::AdAuctionEventRecord event_record);
|
||||
|
||||
// Reports the ad keys to the k-anonymity service. Should be called when
|
||||
// FLEDGE selects an ad.
|
||||
void RegisterAdKeysAsJoined(base::flat_set<std::string> hashed_keys);
|
||||
|
@ -342,6 +342,9 @@ void NetworkServiceClient::OnSharedStorageHeaderReceived(
|
||||
std::move(callback).Run();
|
||||
}
|
||||
|
||||
void NetworkServiceClient::OnAdAuctionEventRecordHeaderReceived(
|
||||
network::AdAuctionEventRecord event_record) {}
|
||||
|
||||
void NetworkServiceClient::Clone(
|
||||
mojo::PendingReceiver<network::mojom::URLLoaderNetworkServiceObserver>
|
||||
observer) {
|
||||
|
@ -132,6 +132,8 @@ class NetworkServiceClient
|
||||
methods_with_options,
|
||||
const std::optional<std::string>& with_lock,
|
||||
OnSharedStorageHeaderReceivedCallback callback) override;
|
||||
void OnAdAuctionEventRecordHeaderReceived(
|
||||
network::AdAuctionEventRecord event_record) override;
|
||||
void Clone(
|
||||
mojo::PendingReceiver<network::mojom::URLLoaderNetworkServiceObserver>
|
||||
listener) override;
|
||||
|
@ -2370,6 +2370,11 @@ void StoragePartitionImpl::OnSharedStorageHeaderReceived(
|
||||
std::move(callback), mojo::GetBadMessageCallback(), /*can_defer=*/true);
|
||||
}
|
||||
|
||||
void StoragePartitionImpl::OnAdAuctionEventRecordHeaderReceived(
|
||||
network::AdAuctionEventRecord event_record) {
|
||||
interest_group_manager_->RecordViewClick(std::move(event_record));
|
||||
}
|
||||
|
||||
void StoragePartitionImpl::Clone(
|
||||
mojo::PendingReceiver<network::mojom::URLLoaderNetworkServiceObserver>
|
||||
observer) {
|
||||
|
@ -398,6 +398,8 @@ class CONTENT_EXPORT StoragePartitionImpl
|
||||
methods_with_options,
|
||||
const std::optional<std::string>& with_lock,
|
||||
OnSharedStorageHeaderReceivedCallback callback) override;
|
||||
void OnAdAuctionEventRecordHeaderReceived(
|
||||
network::AdAuctionEventRecord event_record) override;
|
||||
|
||||
SharedStorageHeaderObserver* shared_storage_header_observer() {
|
||||
return shared_storage_header_observer_.get();
|
||||
|
@ -157,6 +157,9 @@ void UrlLoaderNetworkServiceObserver::OnSharedStorageHeaderReceived(
|
||||
std::move(callback).Run();
|
||||
}
|
||||
|
||||
void UrlLoaderNetworkServiceObserver::OnAdAuctionEventRecordHeaderReceived(
|
||||
network::AdAuctionEventRecord event_record) {}
|
||||
|
||||
void UrlLoaderNetworkServiceObserver::Clone(
|
||||
mojo::PendingReceiver<network::mojom::URLLoaderNetworkServiceObserver>
|
||||
observer) {
|
||||
|
@ -81,6 +81,8 @@ class UrlLoaderNetworkServiceObserver
|
||||
methods_with_options,
|
||||
const std::optional<std::string>& with_lock,
|
||||
OnSharedStorageHeaderReceivedCallback callback) override;
|
||||
void OnAdAuctionEventRecordHeaderReceived(
|
||||
network::AdAuctionEventRecord event_record) override;
|
||||
void Clone(
|
||||
mojo::PendingReceiver<network::mojom::URLLoaderNetworkServiceObserver>
|
||||
listener) override;
|
||||
|
@ -330,6 +330,7 @@ component("network_service") {
|
||||
"//mojo/public/cpp/system",
|
||||
"//net",
|
||||
"//net:extras",
|
||||
"//services/network/ad_auction",
|
||||
"//services/network/attribution",
|
||||
"//services/network/first_party_sets:first_party_sets_manager",
|
||||
"//services/network/public/cpp",
|
||||
|
26
services/network/ad_auction/BUILD.gn
Normal file
26
services/network/ad_auction/BUILD.gn
Normal file
@ -0,0 +1,26 @@
|
||||
# Copyright 2025 The Chromium Authors
|
||||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
source_set("ad_auction") {
|
||||
visibility = [
|
||||
":*",
|
||||
"//services/network:network_service",
|
||||
"//services/network:test_support",
|
||||
"//services/network:tests",
|
||||
]
|
||||
configs += [ "//build/config/compiler:wexit_time_destructors" ]
|
||||
defines = [ "IS_NETWORK_SERVICE_IMPL" ]
|
||||
sources = [
|
||||
"event_record_request_helper.cc",
|
||||
"event_record_request_helper.h",
|
||||
]
|
||||
deps = [
|
||||
"//base",
|
||||
"//net",
|
||||
"//services/network/public/cpp",
|
||||
"//services/network/public/cpp:ad_auction_support",
|
||||
"//services/network/public/mojom",
|
||||
"//url",
|
||||
]
|
||||
}
|
74
services/network/ad_auction/event_record_request_helper.cc
Normal file
74
services/network/ad_auction/event_record_request_helper.cc
Normal file
@ -0,0 +1,74 @@
|
||||
// Copyright 2025 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "services/network/ad_auction/event_record_request_helper.h"
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "base/feature_list.h"
|
||||
#include "base/notreached.h"
|
||||
#include "net/http/structured_headers.h"
|
||||
#include "net/url_request/url_request.h"
|
||||
#include "services/network/public/cpp/ad_auction/event_record.h"
|
||||
#include "services/network/public/cpp/features.h"
|
||||
#include "services/network/public/mojom/ad_auction.mojom.h"
|
||||
#include "services/network/public/mojom/attribution.mojom.h"
|
||||
#include "services/network/public/mojom/url_loader_network_service_observer.mojom.h"
|
||||
#include "url/gurl.h"
|
||||
#include "url/origin.h"
|
||||
|
||||
namespace network {
|
||||
|
||||
void AdAuctionEventRecordRequestHelper::HandleResponse(
|
||||
const net::URLRequest& request) {
|
||||
if (!base::FeatureList::IsEnabled(features::kAdAuctionEventRegistration)) {
|
||||
return;
|
||||
}
|
||||
AdAuctionEventRecord::Type expected_type;
|
||||
switch (attribution_reporting_eligibility_) {
|
||||
case mojom::AttributionReportingEligibility::kEventSource:
|
||||
case mojom::AttributionReportingEligibility::kEventSourceOrTrigger:
|
||||
expected_type = AdAuctionEventRecord::Type::kView;
|
||||
break;
|
||||
case mojom::AttributionReportingEligibility::kNavigationSource:
|
||||
expected_type = AdAuctionEventRecord::Type::kClick;
|
||||
break;
|
||||
case mojom::AttributionReportingEligibility::kUnset:
|
||||
case mojom::AttributionReportingEligibility::kEmpty:
|
||||
case mojom::AttributionReportingEligibility::kTrigger:
|
||||
// Nothing to do for these requests, they're not eligible for click or
|
||||
// view events.
|
||||
return;
|
||||
}
|
||||
|
||||
std::optional<std::string> ad_auction_record_event_header =
|
||||
AdAuctionEventRecord::GetAdAuctionRecordEventHeader(
|
||||
request.response_headers());
|
||||
if (!ad_auction_record_event_header) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::optional<net::structured_headers::Dictionary> dict =
|
||||
net::structured_headers::ParseDictionary(*ad_auction_record_event_header);
|
||||
if (!dict) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::optional<AdAuctionEventRecord> maybe_parsed =
|
||||
AdAuctionEventRecord::MaybeCreateFromStructuredDict(
|
||||
*dict, /*expected_type=*/expected_type,
|
||||
/*providing_origin=*/url::Origin::Create(request.url()));
|
||||
if (!maybe_parsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
url_loader_network_observer_->OnAdAuctionEventRecordHeaderReceived(
|
||||
std::move(*maybe_parsed));
|
||||
}
|
||||
|
||||
} // namespace network
|
47
services/network/ad_auction/event_record_request_helper.h
Normal file
47
services/network/ad_auction/event_record_request_helper.h
Normal file
@ -0,0 +1,47 @@
|
||||
// Copyright 2025 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef SERVICES_NETWORK_AD_AUCTION_EVENT_RECORD_REQUEST_HELPER_H_
|
||||
#define SERVICES_NETWORK_AD_AUCTION_EVENT_RECORD_REQUEST_HELPER_H_
|
||||
|
||||
#include <string_view>
|
||||
|
||||
#include "base/memory/raw_ref.h"
|
||||
#include "net/url_request/url_request.h"
|
||||
#include "services/network/public/mojom/ad_auction.mojom-forward.h"
|
||||
#include "services/network/public/mojom/attribution.mojom.h"
|
||||
#include "services/network/public/mojom/url_loader_network_service_observer.mojom-forward.h"
|
||||
|
||||
namespace network {
|
||||
|
||||
// When constructed for requests with eligible values of
|
||||
// `attribution_reporting_eligibility`, parses Ad-Auction-Record-Event response
|
||||
// headers headers and sends the result, if successful, to
|
||||
// `OnAdAuctionEventRecordHeaderReceived()` on `url_loader_network_observer_`.
|
||||
class AdAuctionEventRecordRequestHelper {
|
||||
public:
|
||||
// `url_loader_network_observer` must outlive this instance.
|
||||
AdAuctionEventRecordRequestHelper(
|
||||
mojom::AttributionReportingEligibility attribution_reporting_eligibility,
|
||||
mojom::URLLoaderNetworkServiceObserver& url_loader_network_observer)
|
||||
: attribution_reporting_eligibility_(attribution_reporting_eligibility),
|
||||
url_loader_network_observer_(url_loader_network_observer) {}
|
||||
|
||||
// If `attribution_reporting_eligibility_` is one of the eligible values,
|
||||
// parses the Ad-Auction-Record-Event header in `request`, sending the result
|
||||
// to `OnAdAuctionEventRecordHeaderReceived()` on
|
||||
// `url_loader_network_observer_`.
|
||||
void HandleResponse(const net::URLRequest& request);
|
||||
|
||||
private:
|
||||
mojom::AttributionReportingEligibility attribution_reporting_eligibility_ =
|
||||
mojom::AttributionReportingEligibility::kUnset;
|
||||
|
||||
const raw_ref<mojom::URLLoaderNetworkServiceObserver>
|
||||
url_loader_network_observer_;
|
||||
};
|
||||
|
||||
} // namespace network
|
||||
|
||||
#endif // SERVICES_NETWORK_AD_AUCTION_EVENT_RECORD_REQUEST_HELPER_H_
|
@ -25,6 +25,7 @@ mojom("mojom_ad_auction") {
|
||||
{
|
||||
mojom = "network.mojom.AdAuctionEventRecord"
|
||||
cpp = "::network::AdAuctionEventRecord"
|
||||
move_only = true
|
||||
},
|
||||
]
|
||||
traits_headers = [
|
||||
@ -636,6 +637,7 @@ mojom("url_loader_base") {
|
||||
|
||||
public_deps = [
|
||||
":cookies_mojom",
|
||||
":mojom_ad_auction",
|
||||
":mojom_attribution",
|
||||
":mojom_content_security_policy",
|
||||
":mojom_integrity_algorithm",
|
||||
|
@ -7,6 +7,7 @@ module network.mojom;
|
||||
import "mojo/public/mojom/base/string16.mojom";
|
||||
import "mojo/public/mojom/base/time.mojom";
|
||||
import "mojo/public/mojom/base/unguessable_token.mojom";
|
||||
import "services/network/public/mojom/ad_auction.mojom";
|
||||
import "services/network/public/mojom/cookie_partition_key.mojom";
|
||||
import "services/network/public/mojom/ip_address.mojom";
|
||||
import "services/network/public/mojom/ip_address_space.mojom";
|
||||
@ -195,8 +196,16 @@ interface URLLoaderNetworkServiceObserver {
|
||||
// triggered by response headers and Shared Storage calls from the same
|
||||
// renderer process where this request originated.
|
||||
OnSharedStorageHeaderReceived(url.mojom.Origin request_origin,
|
||||
array<SharedStorageModifierMethodWithOptions> methods_with_options,
|
||||
network.mojom.LockName? with_lock) => ();
|
||||
array<SharedStorageModifierMethodWithOptions> methods_with_options,
|
||||
network.mojom.LockName? with_lock) => ();
|
||||
|
||||
// Notifies the browser of the results of successful parsing of
|
||||
// a Ad-Auction-Record-Event header. The browser stores information from
|
||||
// `ad_auction_event_record` in the interest group database, for use in
|
||||
// generateBid() browser signals during interest group auctions, providing
|
||||
// bidders view and click signals.
|
||||
OnAdAuctionEventRecordHeaderReceived(
|
||||
AdAuctionEventRecord ad_auction_event_record);
|
||||
|
||||
// Used by the NetworkService to create a copy of this observer.
|
||||
// (e.g. when creating an observer for URLLoader from URLLoaderFactory's
|
||||
|
@ -87,6 +87,9 @@ void TestURLLoaderNetworkObserver::OnSharedStorageHeaderReceived(
|
||||
std::move(callback).Run();
|
||||
}
|
||||
|
||||
void TestURLLoaderNetworkObserver::OnAdAuctionEventRecordHeaderReceived(
|
||||
network::AdAuctionEventRecord event_record) {}
|
||||
|
||||
void TestURLLoaderNetworkObserver::Clone(
|
||||
mojo::PendingReceiver<URLLoaderNetworkServiceObserver> observer) {
|
||||
receivers_.Add(this, std::move(observer));
|
||||
|
@ -72,6 +72,8 @@ class TestURLLoaderNetworkObserver
|
||||
methods_with_options,
|
||||
const std::optional<std::string>& with_lock,
|
||||
OnSharedStorageHeaderReceivedCallback callback) override;
|
||||
void OnAdAuctionEventRecordHeaderReceived(
|
||||
network::AdAuctionEventRecord event_record) override;
|
||||
void Clone(
|
||||
mojo::PendingReceiver<URLLoaderNetworkServiceObserver> observer) override;
|
||||
void OnWebSocketConnectedToPrivateNetwork(
|
||||
|
@ -717,6 +717,9 @@ URLLoader::URLLoader(
|
||||
std::make_unique<SharedStorageRequestHelper>(
|
||||
shared_storage_writable_eligible,
|
||||
url_loader_network_observer_)),
|
||||
ad_auction_event_record_request_helper_(
|
||||
request.attribution_reporting_eligibility,
|
||||
*url_loader_network_observer_),
|
||||
has_fetch_streaming_upload_body_(HasFetchStreamingUploadBody(&request)),
|
||||
accept_ch_frame_observer_(std::move(accept_ch_frame_observer)),
|
||||
allow_cookies_from_browser_(
|
||||
@ -2029,17 +2032,24 @@ void URLLoader::ProcessInboundSharedStorageInterceptorOnResponseStarted() {
|
||||
|
||||
void URLLoader::ProcessInboundAttributionInterceptorOnResponseStarted() {
|
||||
if (!attribution_request_helper_) {
|
||||
ProcessInboundSharedStorageInterceptorOnResponseStarted();
|
||||
ProcessInboundAdAuctionEventRecordInterceptorOnResponseStarted();
|
||||
return;
|
||||
}
|
||||
|
||||
attribution_request_helper_->Finalize(
|
||||
*response_,
|
||||
base::BindOnce(
|
||||
&URLLoader::ProcessInboundSharedStorageInterceptorOnResponseStarted,
|
||||
&URLLoader::
|
||||
ProcessInboundAdAuctionEventRecordInterceptorOnResponseStarted,
|
||||
weak_ptr_factory_.GetWeakPtr()));
|
||||
}
|
||||
|
||||
void URLLoader::
|
||||
ProcessInboundAdAuctionEventRecordInterceptorOnResponseStarted() {
|
||||
ad_auction_event_record_request_helper_.HandleResponse(*url_request_);
|
||||
ProcessInboundSharedStorageInterceptorOnResponseStarted();
|
||||
}
|
||||
|
||||
void URLLoader::OnResponseStarted(net::URLRequest* url_request, int net_error) {
|
||||
DCHECK(url_request == url_request_.get());
|
||||
has_received_response_ = true;
|
||||
|
@ -36,6 +36,7 @@
|
||||
#include "net/socket/socket_tag.h"
|
||||
#include "net/traffic_annotation/network_traffic_annotation.h"
|
||||
#include "net/url_request/url_request.h"
|
||||
#include "services/network/ad_auction/event_record_request_helper.h"
|
||||
#include "services/network/attribution/attribution_request_helper.h"
|
||||
#include "services/network/keepalive_statistics_recorder.h"
|
||||
#include "services/network/network_service.h"
|
||||
@ -412,8 +413,8 @@ class COMPONENT_EXPORT(NETWORK_SERVICE) URLLoader
|
||||
// Storage operations to call
|
||||
// - If the request has not received the `kSharedStorageWriteHeader` response
|
||||
// header, or if parsing fails to produce any valid operations, then
|
||||
// immediately call `ContinueOnReceivedRedirect`
|
||||
// - Otherwise, `ContinueOnReceivedRedirect` will be run asynchronously after
|
||||
// immediately call `ContinueOnReceiveRedirect`
|
||||
// - Otherwise, `ContinueOnReceiveRedirect` will be run asynchronously after
|
||||
// forwarding the operations to `URLLoaderNetworkServiceObserver` to queue via
|
||||
// Mojo
|
||||
//
|
||||
@ -448,7 +449,7 @@ class COMPONENT_EXPORT(NETWORK_SERVICE) URLLoader
|
||||
//
|
||||
// Start in `ProcessOutboundAttributionInterceptor`
|
||||
// - If `attribution_request_helper_` is not defined, immediately
|
||||
// calls`ScheduleStart`.
|
||||
// calls `ScheduleStart`.
|
||||
// - Otherwise:
|
||||
// - Execute `AttributionRequestHelper::Begin`
|
||||
// - On Begin's callback, calls `ScheduleStart`
|
||||
@ -466,18 +467,44 @@ class COMPONENT_EXPORT(NETWORK_SERVICE) URLLoader
|
||||
// Inbound control flow:
|
||||
//
|
||||
// Start in `ProcessInboundAttributionInterceptorOnResponseStarted`
|
||||
// - If `attribution_request_helper_` is not defined, immediately
|
||||
// calls`ProcessInboundSharedStorageInterceptorOnResponseStarted`.
|
||||
// - If `attribution_request_helper_` is not defined,
|
||||
// immediately calls
|
||||
// `ProcessInboundAdAuctionEventRecordInterceptorOnResponseStarted`.
|
||||
// - Otherwise:
|
||||
// - Execute `AttributionRequestHelper::Finalize`
|
||||
// - On Finalize's callback, calls
|
||||
// `ProcessInboundSharedStorageInterceptorOnResponseStarted`
|
||||
// `ProcessInboundAdAuctionEventRecordInterceptorOnResponseStarted`.
|
||||
void ProcessOutboundAttributionInterceptor();
|
||||
void ProcessInboundAttributionInterceptorOnReceivedRedirect(
|
||||
const ::net::RedirectInfo& redirect_info,
|
||||
mojom::URLResponseHeadPtr response);
|
||||
void ProcessInboundAttributionInterceptorOnResponseStarted();
|
||||
|
||||
// All inbound responses will invoke
|
||||
// `ad_auction_event_record_request_helper_.HandleResponse()`, which
|
||||
// always returns immediately without blocking, and also exits early for
|
||||
// ineligible responses.
|
||||
//
|
||||
// Outbound control flow:
|
||||
//
|
||||
// There are no outbound flow methods as the request headers are set by
|
||||
// ComputeAttributionReportingHeaders() in the URLLoader() constructor.
|
||||
//
|
||||
// Redirection control flow:
|
||||
//
|
||||
// Redirection isn't handled yet. TODO(crbug.com/394108643): Support
|
||||
// capturing headers on redirection responses.
|
||||
//
|
||||
// Inbound control flow:
|
||||
//
|
||||
// Start in
|
||||
// `ProcessInboundAdAuctionEventRecordInterceptorOnResponseStarted()`
|
||||
// - Execute
|
||||
// `ad_auction_event_record_request_helper_::HandleResponse()`.
|
||||
// - Afterwards, execute
|
||||
// `ProcessInboundSharedStorageInterceptorOnResponseStarted()`.
|
||||
void ProcessInboundAdAuctionEventRecordInterceptorOnResponseStarted();
|
||||
|
||||
// Continuation of `OnReceivedRedirect` after possibly asynchronously
|
||||
// concluding the request's Attribution and/or Shared Storage operations.
|
||||
void ContinueOnReceiveRedirect(const ::net::RedirectInfo& redirect_info,
|
||||
@ -846,6 +873,11 @@ class COMPONENT_EXPORT(NETWORK_SERVICE) URLLoader
|
||||
// (https://github.com/WICG/shared-storage#from-response-headers).
|
||||
std::unique_ptr<SharedStorageRequestHelper> shared_storage_request_helper_;
|
||||
|
||||
// Request helper responsible for processing Ad Auction record event
|
||||
// headers.
|
||||
// (https://github.com/WICG/turtledove/pull/1279)
|
||||
AdAuctionEventRecordRequestHelper ad_auction_event_record_request_helper_;
|
||||
|
||||
// Indicates |url_request_| is fetch upload request and that has streaming
|
||||
// body.
|
||||
const bool has_fetch_streaming_upload_body_;
|
||||
|
@ -18727,6 +18727,25 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"ProtectedAudienceClickiness": [
|
||||
{
|
||||
"platforms": [
|
||||
"android",
|
||||
"chromeos",
|
||||
"linux",
|
||||
"mac",
|
||||
"windows"
|
||||
],
|
||||
"experiments": [
|
||||
{
|
||||
"name": "Enabled",
|
||||
"enable_features": [
|
||||
"AdAuctionEventRegistration"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ProtectedAudienceDealsSupport": [
|
||||
{
|
||||
"platforms": [
|
||||
|
@ -960,6 +960,7 @@ bool CopyViewAndClickCountsProvidersFromIdlToMojo(
|
||||
provider.Utf8().c_str(), input.name().Utf8().c_str()));
|
||||
return false;
|
||||
}
|
||||
view_and_click_counts_providers.push_back(parsed_provider);
|
||||
}
|
||||
|
||||
output.view_and_click_counts_providers =
|
||||
|
Reference in New Issue
Block a user