0

clickiness: Add ViewAndClickEvents table and read + write logic

This just adds basic functionality and doesn't include things like
full compaction support, expiration, manual clearing logic, etc., as
those will come in subsequent CLs.

The mojom type that will eventually be passed to the worklet process is
defined, but the plumbing to actually pass values to the worklet process
will come in a future CL.

Bug: 394108643
Change-Id: If7948579a78c909c0bd372ac4e5751cb8a86549d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6256880
Reviewed-by: Ayu Ishii <ayui@chromium.org>
Reviewed-by: Orr Bernstein <orrb@google.com>
Commit-Queue: Caleb Raitto <caraitto@chromium.org>
Reviewed-by: Dominic Farolino <dom@chromium.org>
Reviewed-by: Cammie Smith Barnes <cammie@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1432359}
This commit is contained in:
Caleb Raitto
2025-03-13 14:05:34 -07:00
committed by Chromium LUCI CQ
parent eec8f69cfd
commit 04f5ab713b
12 changed files with 1083 additions and 22 deletions

@ -2678,8 +2678,12 @@ class AuctionRunnerTest : public RenderViewHostTestHarness,
StorageInterestGroup storage_group;
storage_group.interest_group = std::move(interest_group);
storage_group.bidding_browser_signals =
blink::mojom::BiddingBrowserSignals::New(3, 5, std::move(previous_wins),
false);
blink::mojom::BiddingBrowserSignals::New(
3, 5, std::move(previous_wins), false,
/*click_and_view_counts=*/
blink::mojom::ViewAndClickCounts::New(
/*view_counts=*/blink::mojom::ViewOrClickCounts::New(),
/*click_counts=*/blink::mojom::ViewOrClickCounts::New()));
storage_group.joining_origin = storage_group.interest_group.owner;
return storage_group;
}

@ -46,8 +46,12 @@ StorageInterestGroup MakeInterestGroup(blink::InterestGroup interest_group) {
StorageInterestGroup storage_group;
storage_group.interest_group = std::move(interest_group);
storage_group.bidding_browser_signals =
blink::mojom::BiddingBrowserSignals::New(3, 5, std::move(previous_wins),
false);
blink::mojom::BiddingBrowserSignals::New(
3, 5, std::move(previous_wins), false,
/*click_and_view_counts=*/
blink::mojom::ViewAndClickCounts::New(
/*view_counts=*/blink::mojom::ViewOrClickCounts::New(),
/*click_counts=*/blink::mojom::ViewOrClickCounts::New()));
storage_group.joining_origin = storage_group.interest_group.owner;
return storage_group;
}

@ -46,6 +46,7 @@
#include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom.h"
#include "crypto/sha2.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "services/network/public/cpp/ad_auction/event_record.h"
#include "services/network/public/cpp/features.h"
#include "sql/database.h"
#include "sql/error_delegate_util.h"
@ -69,7 +70,19 @@ namespace {
using PassKey = base::PassKey<InterestGroupStorage>;
using blink::mojom::BiddingBrowserSignalsPtr;
using blink::mojom::PreviousWinPtr;
using blink::mojom::ViewAndClickCountsPtr;
using blink::mojom::ViewOrClickCountsPtr;
using SellerCapabilitiesType = blink::SellerCapabilitiesType;
using network::AdAuctionEventRecord;
// The raw view and click data for a given (provider_origin, eligible_origin)
// tuple.
struct ViewClickCountsForProviderAndEligible {
ListOfTimestamps uncompacted_view_events;
ListOfTimestampAndCounts compacted_view_events;
ListOfTimestamps uncompacted_click_events;
ListOfTimestampAndCounts compacted_click_events;
};
const base::FilePath::CharType kDatabasePath[] =
FILE_PATH_LITERAL("InterestGroups");
@ -107,6 +120,7 @@ const base::FilePath::CharType kDatabasePath[] =
// Version 31 - 2025/01 - crrev.com/c/6084483
// Version 32 - 2025/02 - crrev.com/c/6239846
// Version 33 - 2025/02 - crrev.com/c/6248184
// Version 34 - 2025/02 - crrev.com/c/6256880
//
// Version 1 adds a table for interest groups.
// Version 2 adds a column for rate limiting interest group updates.
@ -151,8 +165,9 @@ const base::FilePath::CharType kDatabasePath[] =
// Version 32 adds duration column to the debug report lockout table, and
// renames its last_report_sent_time column to starting_time.
// Version 33 adds view_and_click_counts_providers interest_groups field.
// Version 34 adds view_and_click_events table.
const int kCurrentVersionNumber = 33;
const int kCurrentVersionNumber = 34;
// Earliest version of the code which can use a |kCurrentVersionNumber| database
// without failing.
@ -1169,6 +1184,26 @@ bool CreateCurrentSchema(sql::Database& db) {
if (!db.Execute(kBAKeysTableSql)) {
return false;
}
DCHECK(!db.DoesTableExist("view_and_click_events"));
static const char kViewAndClickEventsSql[] =
// clang-format off
"CREATE TABLE view_and_click_events("
"provider_origin TEXT NOT NULL,"
"eligible_origin TEXT NOT NULL,"
"uncompacted_view_events BLOB NOT NULL,"
"compacted_view_events BLOB NOT NULL,"
"uncompacted_click_events BLOB NOT NULL,"
"compacted_click_events BLOB NOT NULL,"
"PRIMARY KEY(provider_origin, eligible_origin))";
// clang-format on
if (!db.Execute(kViewAndClickEventsSql)) {
DLOG(ERROR)
<< "view_and_click_events CREATE SQL statement did not compile: "
<< db.GetErrorMessage();
return false;
}
return true;
}
@ -1177,6 +1212,30 @@ bool VacuumDB(sql::Database& db) {
return db.Execute(kVacuum);
}
bool UpgradeV33SchemaToV34(sql::Database& db, sql::MetaTable& meta_table) {
// Make `view_and_click_events` table.
DCHECK(!db.DoesTableExist("view_and_click_events"));
static const char kViewAndClickEventsSql[] =
// clang-format off
"CREATE TABLE view_and_click_events("
"provider_origin TEXT NOT NULL,"
"eligible_origin TEXT NOT NULL,"
"uncompacted_view_events BLOB NOT NULL,"
"compacted_view_events BLOB NOT NULL,"
"uncompacted_click_events BLOB NOT NULL,"
"compacted_click_events BLOB NOT NULL,"
"PRIMARY KEY(provider_origin, eligible_origin))";
// clang-format on
if (!db.Execute(kViewAndClickEventsSql)) {
DLOG(ERROR) << "view_and_click_counts_providers upgrade CREATE SQL "
"statement did not compile: "
<< db.GetErrorMessage();
return false;
}
return true;
}
bool UpgradeV32SchemaToV33(sql::Database& db, sql::MetaTable& meta_table) {
// Make a table with new column `view_and_click_counts_providers`.
static const char kInterestGroupTableSql[] =
@ -3200,6 +3259,11 @@ bool UpgradeDB(sql::Database& db,
if (!UpgradeV32SchemaToV33(db, meta_table)) {
return false;
}
[[fallthrough]];
case 33:
if (!UpgradeV33SchemaToV34(db, meta_table)) {
return false;
}
if (!meta_table.SetVersionNumber(kCurrentVersionNumber)) {
return false;
}
@ -4487,6 +4551,304 @@ void DoUpdateLastKAnonymityReported(sql::Database& db,
transaction.Commit();
}
// Loads the view and click data for `provider_origin`, `eligible_origin`.
// Returns std::nullopt on failure, and an empty
// `ViewClickCountsForProviderAndEligible` if no counts are stored for the given
// (`provider_origin`, `eligible_origin`) tuple.
std::optional<ViewClickCountsForProviderAndEligible>
DoGetViewClickCountsForProviderAndEligible(sql::Database& db,
const url::Origin& provider_origin,
const url::Origin& eligible_origin) {
ViewClickCountsForProviderAndEligible result;
sql::Statement get_counts(
db.GetCachedStatement(SQL_FROM_HERE,
"SELECT uncompacted_view_events,"
"compacted_view_events,"
"uncompacted_click_events,"
"compacted_click_events "
"FROM view_and_click_events "
"WHERE provider_origin=? AND eligible_origin=?"));
if (!get_counts.is_valid()) {
DLOG(ERROR) << "GetViewClickCountsForProviderAndEligible SQL statement did "
"not compile: "
<< db.GetErrorMessage();
return std::nullopt;
}
get_counts.Reset(true);
get_counts.BindString(0, provider_origin.Serialize());
get_counts.BindString(1, eligible_origin.Serialize());
if (!get_counts.Step()) {
// No counts stored, return empty counts lists.
return result;
}
if (!get_counts.Succeeded()) {
return std::nullopt;
}
if (!result.uncompacted_view_events.ParseFromString(
get_counts.ColumnString(0)) ||
!result.compacted_view_events.ParseFromString(
get_counts.ColumnString(1)) ||
!result.uncompacted_click_events.ParseFromString(
get_counts.ColumnString(2)) ||
!result.compacted_click_events.ParseFromString(
get_counts.ColumnString(3))) {
base::UmaHistogramEnumeration(
"Storage.InterestGroup.ProtoDeserializationResult.ListOfTimestamps",
InterestGroupStorageProtoSerializationResult::kFailed);
return std::nullopt;
} else {
base::UmaHistogramEnumeration(
"Storage.InterestGroup.ProtoDeserializationResult.ListOfTimestamps",
InterestGroupStorageProtoSerializationResult::kSucceeded);
}
return result;
}
void DoIncrementViewClickCounts(base::Time now,
ViewOrClickCountsPtr& view_or_click_counts,
base::TimeDelta max_window,
int64_t int_timestamp,
int32_t count) {
base::TimeDelta event_age = now - base::Time::FromDeltaSinceWindowsEpoch(
base::Microseconds(int_timestamp));
if (max_window >= base::Hours(1) && event_age <= base::Hours(1)) {
view_or_click_counts->past_hour += count;
}
if (max_window >= base::Days(1) && event_age <= base::Days(1)) {
view_or_click_counts->past_day += count;
}
if (max_window >= base::Days(7) && event_age <= base::Days(7)) {
view_or_click_counts->past_week += count;
}
if (max_window >= base::Days(30) && event_age <= base::Days(30)) {
view_or_click_counts->past_30_days += count;
}
if (max_window >= base::Days(90) && event_age <= base::Days(90)) {
view_or_click_counts->past_90_days += count;
}
// Older expired events may exist -- maintenance will eventually remove them.
// TODO(crbug.com/394108643): Implement click / view removal logic.
}
// Mutates `group`'s browser signals, filling in loaded view and click counts.
// Returns true on success, and false on failure.
[[nodiscard]] bool DoGetViewAndClickCountsForGroup(
sql::Database& db,
base::Time now,
StorageInterestGroup& group) {
ViewAndClickCountsPtr& view_and_click_counts =
group.bidding_browser_signals->view_and_click_counts;
view_and_click_counts = blink::mojom::ViewAndClickCounts::New(
/*view_counts=*/blink::mojom::ViewOrClickCounts::New(),
/*click_counts=*/blink::mojom::ViewOrClickCounts::New());
if (!group.interest_group.view_and_click_counts_providers) {
return true;
}
const base::TimeDelta max_window = blink::MaxInterestGroupLifetime();
for (const url::Origin& provider_origin :
*group.interest_group.view_and_click_counts_providers) {
std::optional<ViewClickCountsForProviderAndEligible> partial_counts =
DoGetViewClickCountsForProviderAndEligible(
/*db=*/db,
/*provider_origin=*/provider_origin,
/*eligible_origin=*/group.interest_group.owner);
if (!partial_counts) {
return false;
}
for (int64_t timestamp :
partial_counts->uncompacted_view_events.timestamps()) {
DoIncrementViewClickCounts(
/*now=*/now,
/*view_or_click_counts=*/view_and_click_counts->view_counts,
/*max_window=*/max_window,
/*int_timestamp=*/timestamp, /*count=*/1);
}
for (ListOfTimestampAndCounts_Entry timestamp_and_count :
partial_counts->compacted_view_events.timestamp_and_counts()) {
DoIncrementViewClickCounts(
/*now=*/now,
/*view_or_click_counts=*/view_and_click_counts->view_counts,
/*max_window=*/max_window,
/*int_timestamp=*/timestamp_and_count.timestamp(),
/*count=*/timestamp_and_count.count());
}
for (int64_t timestamp :
partial_counts->uncompacted_click_events.timestamps()) {
DoIncrementViewClickCounts(
/*now=*/now,
/*view_or_click_counts=*/view_and_click_counts->click_counts,
/*max_window=*/max_window,
/*int_timestamp=*/timestamp, /*count=*/1);
}
for (ListOfTimestampAndCounts_Entry timestamp_and_count :
partial_counts->compacted_click_events.timestamp_and_counts()) {
DoIncrementViewClickCounts(
/*now=*/now,
/*view_or_click_counts=*/view_and_click_counts->click_counts,
/*max_window=*/max_window,
/*int_timestamp=*/timestamp_and_count.timestamp(),
/*count=*/timestamp_and_count.count());
}
}
return true;
}
void DoRecordViewClick(sql::Database& db,
const network::AdAuctionEventRecord& record,
base::Time now) {
sql::Transaction transaction(&db);
if (!transaction.Begin()) {
return;
}
const std::vector<url::Origin>* eligible_origins = nullptr;
std::vector<url::Origin> default_eligible_origin;
if (record.eligible_origins.empty()) {
default_eligible_origin.emplace_back(record.providing_origin);
eligible_origins = &default_eligible_origin;
} else {
eligible_origins = &record.eligible_origins;
}
for (const url::Origin& eligible_origin : *eligible_origins) {
sql::Statement get_counts(
db.GetCachedStatement(SQL_FROM_HERE,
"SELECT uncompacted_view_events,"
"uncompacted_click_events "
"FROM view_and_click_events "
"WHERE provider_origin=? AND eligible_origin=?"));
if (!get_counts.is_valid()) {
DLOG(ERROR) << "DoRecordViewClick SELECT SQL statement did not compile: "
<< db.GetErrorMessage();
return;
}
get_counts.Reset(true);
get_counts.BindString(0, record.providing_origin.Serialize());
get_counts.BindString(1, eligible_origin.Serialize());
bool row_exists = false;
ListOfTimestamps uncompacted_view_events;
ListOfTimestamps uncompacted_click_events;
if (get_counts.Step()) {
row_exists = true;
if (!uncompacted_view_events.ParseFromString(
get_counts.ColumnString(0)) ||
!uncompacted_click_events.ParseFromString(
get_counts.ColumnString(1))) {
// TODO(crbug.com/355010821): Consider bubbling out the failure.
base::UmaHistogramEnumeration(
"Storage.InterestGroup.ProtoDeserializationResult.ListOfTimestamps",
InterestGroupStorageProtoSerializationResult::kFailed);
return;
} else {
base::UmaHistogramEnumeration(
"Storage.InterestGroup.ProtoDeserializationResult.ListOfTimestamps",
InterestGroupStorageProtoSerializationResult::kSucceeded);
}
} else if (!get_counts.Succeeded()) {
return;
}
// TODO(crbug.com/394108643): enforce rate limit.
const int64_t int_now = now.ToDeltaSinceWindowsEpoch().InMicroseconds();
switch (record.type) {
case AdAuctionEventRecord::Type::kUninitialized:
NOTREACHED();
case AdAuctionEventRecord::Type::kView:
uncompacted_view_events.add_timestamps(int_now);
break;
case AdAuctionEventRecord::Type::kClick:
uncompacted_click_events.add_timestamps(int_now);
break;
}
std::string uncompacted_view_events_str;
std::string uncompacted_click_events_str;
if (!uncompacted_view_events.SerializeToString(
&uncompacted_view_events_str) ||
!uncompacted_click_events.SerializeToString(
&uncompacted_click_events_str)) {
base::UmaHistogramEnumeration(
"Storage.InterestGroup.ProtoSerializationResult.ListOfTimestamps",
InterestGroupStorageProtoSerializationResult::kFailed);
// TODO(crbug.com/355010821): Consider bubbling out the failure.
return;
} else {
base::UmaHistogramEnumeration(
"Storage.InterestGroup.ProtoSerializationResult.ListOfTimestamps",
InterestGroupStorageProtoSerializationResult::kSucceeded);
}
if (!row_exists) {
// clang-format off
sql::Statement insert_counts(
db.GetCachedStatement(SQL_FROM_HERE,
"INSERT INTO view_and_click_events("
"provider_origin,"
"eligible_origin,"
"uncompacted_view_events,"
"compacted_view_events,"
"uncompacted_click_events,"
"compacted_click_events) "
"VALUES(?,?,?,?,?,?)"));
// clang-format on
if (!insert_counts.is_valid()) {
DLOG(ERROR)
<< "DoRecordViewClick INSERT SQL statement did not compile: "
<< db.GetErrorMessage();
return;
}
insert_counts.Reset(true);
insert_counts.BindString(0, record.providing_origin.Serialize());
insert_counts.BindString(1, eligible_origin.Serialize());
insert_counts.BindString(2, uncompacted_view_events_str);
insert_counts.BindString(3, "");
insert_counts.BindString(4, uncompacted_click_events_str);
insert_counts.BindString(5, "");
if (!insert_counts.Run()) {
return;
}
} else {
// clang-format off
sql::Statement update_counts(
db.GetCachedStatement(SQL_FROM_HERE,
"UPDATE view_and_click_events "
"SET uncompacted_view_events=?,"
"uncompacted_click_events=? "
"WHERE provider_origin=? AND eligible_origin=?"));
// clang-format on
if (!update_counts.is_valid()) {
DLOG(ERROR)
<< "DoRecordViewClick UPDATE SQL statement did not compile: "
<< db.GetErrorMessage();
return;
}
update_counts.Reset(true);
update_counts.BindString(0, uncompacted_view_events_str);
update_counts.BindString(1, uncompacted_click_events_str);
update_counts.BindString(2, record.providing_origin.Serialize());
update_counts.BindString(3, eligible_origin.Serialize());
if (!update_counts.Run()) {
return;
}
}
}
transaction.Commit();
}
std::optional<std::vector<url::Origin>> DoGetAllInterestGroupOwners(
sql::Database& db,
base::Time expiring_after) {
@ -4896,9 +5258,15 @@ bool DoGetStoredInterestGroup(sql::Database& db,
db_interest_group.bidding_browser_signals)) {
return false;
}
return GetPreviousWins(db, group_key,
now - blink::MaxInterestGroupLifetimeForMetadata(),
db_interest_group.bidding_browser_signals);
if (!GetPreviousWins(db, group_key,
now - blink::MaxInterestGroupLifetimeForMetadata(),
db_interest_group.bidding_browser_signals)) {
return false;
}
if (!DoGetViewAndClickCountsForGroup(db, now, db_interest_group)) {
return false;
}
return true;
}
std::optional<std::vector<InterestGroupUpdateParameter>>
@ -5105,6 +5473,20 @@ std::optional<std::vector<StorageInterestGroup>> DoGetInterestGroupsForOwner(
return std::nullopt;
}
}
{
TRACE_EVENT("fledge", "load_from_clicks_views_table");
// TODO(crbug.com/394108643): For performance, consider loading all of the
// view_and_click_counts for a given providing_origin/eligible_origin pair,
// and then aggregating across all of the providing_origins for each
// interest group with a matching eligible_origin. This prevents having to
// load the same records again and again for each interest group of a given
// IG owner.
for (auto& [unused_name, storage_group] : interest_group_by_name) {
if (!DoGetViewAndClickCountsForGroup(db, now, storage_group)) {
return std::nullopt;
}
}
}
if (!transaction.Commit()) {
return std::nullopt;
}
@ -6128,6 +6510,16 @@ void InterestGroupStorage::UpdateLastKAnonymityReported(
DoUpdateLastKAnonymityReported(*db_, hashed_key, base::Time::Now());
}
void InterestGroupStorage::RecordViewClick(
const network::AdAuctionEventRecord& record) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!EnsureDBInitialized()) {
return;
}
DoRecordViewClick(*db_, record, base::Time::Now());
}
std::optional<StorageInterestGroup> InterestGroupStorage::GetInterestGroup(
const blink::InterestGroupKey& group_key) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

@ -30,6 +30,9 @@
namespace blink {
struct InterestGroup;
}
namespace network {
struct AdAuctionEventRecord;
}
namespace content {
// InterestGroupStorage controls access to the Interest Group Database. All
@ -150,6 +153,10 @@ class CONTENT_EXPORT InterestGroupStorage {
// Updates the last time that the key was reported to the k-anonymity server.
void UpdateLastKAnonymityReported(const std::string& hashed_key);
// Stores the view or click data in `record` so that it may be later included
// in view / click counts loaded for generateBid() browser signals.
void RecordViewClick(const network::AdAuctionEventRecord& record);
// Gets a single interest group.
std::optional<StorageInterestGroup> GetInterestGroup(
const blink::InterestGroupKey& group_key);

@ -47,3 +47,15 @@ message KAnonKeyProtos {
message ListOfOrigins {
repeated string origins = 1;
}
message ListOfTimestamps {
repeated int64 timestamps = 1;
}
message ListOfTimestampAndCounts {
message Entry {
optional int64 timestamp = 1;
optional int64 count = 2;
}
repeated Entry timestamp_and_counts = 1;
}

@ -37,6 +37,7 @@
#include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom.h"
#include "crypto/sha2.h"
#include "services/network/network_service.h"
#include "services/network/public/cpp/ad_auction/event_record.h"
#include "services/network/public/cpp/features.h"
#include "sql/database.h"
#include "sql/meta_table.h"
@ -56,6 +57,7 @@ namespace {
using ::blink::IgExpectEqualsForTesting;
using ::blink::IgExpectNotEqualsForTesting;
using ::blink::InterestGroup;
using ::network::AdAuctionEventRecord;
using ::testing::Field;
using ::testing::Property;
using ::testing::UnorderedElementsAre;
@ -66,6 +68,15 @@ using SellerCapabilitiesType = blink::SellerCapabilitiesType;
constexpr char kFullOriginStr[] = "https://full.example.com";
constexpr char kPartialOriginStr[] = "https://partial.example.com";
constexpr char kViewClickEligibleOrigin1Str[] =
"https://view-click.eligible1.test";
constexpr char kViewClickEligibleOrigin2Str[] =
"https://view-click.eligible2.test";
constexpr char kViewClickProviderOrigin1Str[] =
"https://view-click.provider1.test";
constexpr char kViewClickProviderOrigin2Str[] =
"https://view-click.provider2.test";
constexpr int kOldestAllFieldsVersion = 28;
class InterestGroupStorageTest : public testing::Test {
@ -182,6 +193,7 @@ class InterestGroupStorageTest : public testing::Test {
case 27:
case 30:
case 32:
case 34:
*version_changed_ig_fields = false;
break;
default:
@ -214,6 +226,10 @@ class InterestGroupStorageTest : public testing::Test {
// instance.
switch (version_number) {
case 34:
// Added `view_and_click_events` table, but introduced no new
// `interest_group` table fields.
[[fallthrough]];
case 33:
result.view_and_click_counts_providers = {{url::Origin::Create(
GURL("https://view-and-click-counts-provider.test"))}};
@ -402,6 +418,15 @@ class InterestGroupStorageTest : public testing::Test {
const url::Origin kPartialOrigin =
url::Origin::Create(GURL(kPartialOriginStr));
const url::Origin kViewClickEligibleOrigin1 =
url::Origin::Create(GURL(kViewClickEligibleOrigin1Str));
const url::Origin kViewClickEligibleOrigin2 =
url::Origin::Create(GURL(kViewClickEligibleOrigin2Str));
const url::Origin kViewClickProviderOrigin1 =
url::Origin::Create(GURL(kViewClickProviderOrigin1Str));
const url::Origin kViewClickProviderOrigin2 =
url::Origin::Create(GURL(kViewClickProviderOrigin2Str));
base::ScopedTempDir temp_directory_;
private:
@ -435,8 +460,9 @@ TEST_F(InterestGroupStorageTest, DatabaseInitialized_CreateDatabase) {
// [interest_groups], [join_history], [bid_history], [win_history],
// [joined_k_anon], [meta], [lockout_debugging_only_report],
// [cooldown_debugging_only_report], [bidding_and_auction_server_keys].
EXPECT_EQ(9u, sql::test::CountSQLTables(&raw_db)) << raw_db.GetSchema();
// [cooldown_debugging_only_report], [bidding_and_auction_server_keys],
// [view_and_click_events].
EXPECT_EQ(10u, sql::test::CountSQLTables(&raw_db)) << raw_db.GetSchema();
}
}
@ -472,8 +498,9 @@ TEST_F(InterestGroupStorageTest, DatabaseRazesOldVersion) {
// [interest_groups], [join_history], [bid_history], [win_history],
// [joined_k_anon], [meta], [lockout_debugging_only_report],
// [cooldown_debugging_only_report], [bidding_and_auction_server_keys].
EXPECT_EQ(9u, sql::test::CountSQLTables(&raw_db)) << raw_db.GetSchema();
// [cooldown_debugging_only_report], [bidding_and_auction_server_keys],
// [view_and_click_events].
EXPECT_EQ(10u, sql::test::CountSQLTables(&raw_db)) << raw_db.GetSchema();
}
}
@ -509,8 +536,9 @@ TEST_F(InterestGroupStorageTest, DatabaseRazesNewVersion) {
// [interest_groups], [join_history], [bid_history], [win_history],
// [joined_k_anon], [meta], [lockout_debugging_only_report],
// [cooldown_debugging_only_report], [bidding_and_auction_server_keys].
EXPECT_EQ(9u, sql::test::CountSQLTables(&raw_db)) << raw_db.GetSchema();
// [cooldown_debugging_only_report], [bidding_and_auction_server_keys],
// [view_and_click_events].
EXPECT_EQ(10u, sql::test::CountSQLTables(&raw_db)) << raw_db.GetSchema();
}
}
@ -2033,6 +2061,544 @@ TEST_F(InterestGroupStorageTest, KAnonDataExpires) {
EXPECT_TRUE(groups[0].hashed_kanon_keys.empty());
}
enum class GroupLifetime {
k30Day,
k90Day,
};
class InterestGroupStorageDualLifetimeTest
: public InterestGroupStorageTest,
public ::testing::WithParamInterface<GroupLifetime> {
public:
void SetUp() override {
InterestGroupStorageTest::SetUp();
switch (GetParam()) {
case GroupLifetime::k30Day:
scoped_feature_list_.InitAndEnableFeatureWithParameters(
blink::features::kFledgeMaxGroupLifetimeFeature,
{{"fledge_max_group_lifetime", "30d"}});
break;
case GroupLifetime::k90Day:
scoped_feature_list_.InitAndEnableFeatureWithParameters(
blink::features::kFledgeMaxGroupLifetimeFeature,
{{"fledge_max_group_lifetime", "90d"}});
break;
}
}
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_P(InterestGroupStorageDualLifetimeTest, ViewClickStoreRetrieve_Basic) {
std::unique_ptr<InterestGroupStorage> storage = CreateStorage();
AdAuctionEventRecord record;
record.type = AdAuctionEventRecord::Type::kView;
record.providing_origin = kViewClickProviderOrigin1;
record.eligible_origins = {kViewClickEligibleOrigin1};
ASSERT_TRUE(record.IsValid());
storage->RecordViewClick(record);
InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars");
g.view_and_click_counts_providers = {{kViewClickProviderOrigin1}};
g.expiry = base::Time::Now() + base::Days(90);
storage->JoinInterestGroup(g, GURL("https://joining-site.test"));
std::vector<StorageInterestGroup> groups =
storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1);
ASSERT_EQ(1u, groups.size());
blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
groups[0].bidding_browser_signals->view_and_click_counts;
EXPECT_EQ(1, view_and_click_counts->view_counts->past_hour);
EXPECT_EQ(1, view_and_click_counts->view_counts->past_day);
EXPECT_EQ(1, view_and_click_counts->view_counts->past_week);
EXPECT_EQ(1, view_and_click_counts->view_counts->past_30_days);
EXPECT_EQ(GetParam() == GroupLifetime::k90Day ? 1 : 0,
view_and_click_counts->view_counts->past_90_days);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_hour);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_day);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_week);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_30_days);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_90_days);
}
TEST_P(InterestGroupStorageDualLifetimeTest,
ViewClickStoreRetrieve_BasicByGroupKey) {
std::unique_ptr<InterestGroupStorage> storage = CreateStorage();
AdAuctionEventRecord record;
record.type = AdAuctionEventRecord::Type::kView;
record.providing_origin = kViewClickProviderOrigin1;
record.eligible_origins = {kViewClickEligibleOrigin1};
ASSERT_TRUE(record.IsValid());
storage->RecordViewClick(record);
InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars");
g.view_and_click_counts_providers = {{kViewClickProviderOrigin1}};
g.expiry = base::Time::Now() + base::Days(90);
storage->JoinInterestGroup(g, GURL("https://joining-site.test"));
std::optional<StorageInterestGroup> group = storage->GetInterestGroup(
blink::InterestGroupKey(kViewClickEligibleOrigin1, "cars"));
ASSERT_TRUE(group);
blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
group->bidding_browser_signals->view_and_click_counts;
EXPECT_EQ(1, view_and_click_counts->view_counts->past_hour);
EXPECT_EQ(1, view_and_click_counts->view_counts->past_day);
EXPECT_EQ(1, view_and_click_counts->view_counts->past_week);
EXPECT_EQ(1, view_and_click_counts->view_counts->past_30_days);
EXPECT_EQ(GetParam() == GroupLifetime::k90Day ? 1 : 0,
view_and_click_counts->view_counts->past_90_days);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_hour);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_day);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_week);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_30_days);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_90_days);
}
TEST_P(InterestGroupStorageDualLifetimeTest, ViewClickStoreRetrieve_NoEvents) {
std::unique_ptr<InterestGroupStorage> storage = CreateStorage();
InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars");
g.view_and_click_counts_providers = {{kViewClickProviderOrigin1}};
g.expiry = base::Time::Now() + base::Days(90);
storage->JoinInterestGroup(g, GURL("https://joining-site.test"));
std::vector<StorageInterestGroup> groups =
storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1);
ASSERT_EQ(1u, groups.size());
blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
groups[0].bidding_browser_signals->view_and_click_counts;
EXPECT_EQ(0, view_and_click_counts->view_counts->past_hour);
EXPECT_EQ(0, view_and_click_counts->view_counts->past_day);
EXPECT_EQ(0, view_and_click_counts->view_counts->past_week);
EXPECT_EQ(0, view_and_click_counts->view_counts->past_30_days);
EXPECT_EQ(0, view_and_click_counts->view_counts->past_90_days);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_hour);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_day);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_week);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_30_days);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_90_days);
}
TEST_P(InterestGroupStorageDualLifetimeTest,
ViewClickStoreRetrieve_NoEventsNoProvider) {
std::unique_ptr<InterestGroupStorage> storage = CreateStorage();
InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars");
g.expiry = base::Time::Now() + base::Days(90);
storage->JoinInterestGroup(g, GURL("https://joining-site.test"));
std::vector<StorageInterestGroup> groups =
storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1);
ASSERT_EQ(1u, groups.size());
blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
groups[0].bidding_browser_signals->view_and_click_counts;
EXPECT_EQ(0, view_and_click_counts->view_counts->past_hour);
EXPECT_EQ(0, view_and_click_counts->view_counts->past_day);
EXPECT_EQ(0, view_and_click_counts->view_counts->past_week);
EXPECT_EQ(0, view_and_click_counts->view_counts->past_30_days);
EXPECT_EQ(0, view_and_click_counts->view_counts->past_90_days);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_hour);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_day);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_week);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_30_days);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_90_days);
}
TEST_P(InterestGroupStorageDualLifetimeTest,
ViewClickStoreRetrieve_EligibleDefaultsToProvider) {
std::unique_ptr<InterestGroupStorage> storage = CreateStorage();
AdAuctionEventRecord record;
record.type = AdAuctionEventRecord::Type::kView;
record.providing_origin = kViewClickProviderOrigin1;
record.eligible_origins = {};
ASSERT_TRUE(record.IsValid());
storage->RecordViewClick(record);
InterestGroup g = NewInterestGroup(kViewClickProviderOrigin1, "cars");
g.view_and_click_counts_providers = {{kViewClickProviderOrigin1}};
g.expiry = base::Time::Now() + base::Days(90);
storage->JoinInterestGroup(g, GURL("https://joining-site.test"));
std::vector<StorageInterestGroup> groups =
storage->GetInterestGroupsForOwner(kViewClickProviderOrigin1);
ASSERT_EQ(1u, groups.size());
blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
groups[0].bidding_browser_signals->view_and_click_counts;
EXPECT_EQ(1, view_and_click_counts->view_counts->past_hour);
EXPECT_EQ(1, view_and_click_counts->view_counts->past_day);
EXPECT_EQ(1, view_and_click_counts->view_counts->past_week);
EXPECT_EQ(1, view_and_click_counts->view_counts->past_30_days);
EXPECT_EQ(GetParam() == GroupLifetime::k90Day ? 1 : 0,
view_and_click_counts->view_counts->past_90_days);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_hour);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_day);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_week);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_30_days);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_90_days);
}
TEST_P(InterestGroupStorageDualLifetimeTest,
ViewClickStoreRetrieve_ProviderNotInEligibleOrigins) {
std::unique_ptr<InterestGroupStorage> storage = CreateStorage();
AdAuctionEventRecord record;
record.type = AdAuctionEventRecord::Type::kView;
record.providing_origin = kViewClickProviderOrigin1;
record.eligible_origins = {kViewClickEligibleOrigin1};
ASSERT_TRUE(record.IsValid());
storage->RecordViewClick(record);
InterestGroup g = NewInterestGroup(kViewClickProviderOrigin1, "cars");
g.view_and_click_counts_providers = {{kViewClickProviderOrigin1}};
g.expiry = base::Time::Now() + base::Days(90);
storage->JoinInterestGroup(g, GURL("https://joining-site.test"));
std::vector<StorageInterestGroup> groups =
storage->GetInterestGroupsForOwner(kViewClickProviderOrigin1);
ASSERT_EQ(1u, groups.size());
blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
groups[0].bidding_browser_signals->view_and_click_counts;
EXPECT_EQ(0, view_and_click_counts->view_counts->past_hour);
EXPECT_EQ(0, view_and_click_counts->view_counts->past_day);
EXPECT_EQ(0, view_and_click_counts->view_counts->past_week);
EXPECT_EQ(0, view_and_click_counts->view_counts->past_30_days);
EXPECT_EQ(0, view_and_click_counts->view_counts->past_90_days);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_hour);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_day);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_week);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_30_days);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_90_days);
}
TEST_P(InterestGroupStorageDualLifetimeTest,
ViewClickStoreRetrieve_BasicWithTwoEvents) {
std::unique_ptr<InterestGroupStorage> storage = CreateStorage();
AdAuctionEventRecord record;
record.type = AdAuctionEventRecord::Type::kView;
record.providing_origin = kViewClickProviderOrigin1;
record.eligible_origins = {kViewClickEligibleOrigin1};
ASSERT_TRUE(record.IsValid());
storage->RecordViewClick(record);
storage->RecordViewClick(record);
InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars");
g.view_and_click_counts_providers = {{kViewClickProviderOrigin1}};
g.expiry = base::Time::Now() + base::Days(90);
storage->JoinInterestGroup(g, GURL("https://joining-site.test"));
std::vector<StorageInterestGroup> groups =
storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1);
ASSERT_EQ(1u, groups.size());
blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
groups[0].bidding_browser_signals->view_and_click_counts;
EXPECT_EQ(2, view_and_click_counts->view_counts->past_hour);
EXPECT_EQ(2, view_and_click_counts->view_counts->past_day);
EXPECT_EQ(2, view_and_click_counts->view_counts->past_week);
EXPECT_EQ(2, view_and_click_counts->view_counts->past_30_days);
EXPECT_EQ(GetParam() == GroupLifetime::k90Day ? 2 : 0,
view_and_click_counts->view_counts->past_90_days);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_hour);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_day);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_week);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_30_days);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_90_days);
}
TEST_P(InterestGroupStorageDualLifetimeTest,
ViewClickStoreRetrieve_BucketsTime) {
std::unique_ptr<InterestGroupStorage> storage = CreateStorage();
AdAuctionEventRecord record_view;
record_view.type = AdAuctionEventRecord::Type::kView;
record_view.providing_origin = kViewClickProviderOrigin1;
record_view.eligible_origins = {kViewClickEligibleOrigin1};
ASSERT_TRUE(record_view.IsValid());
AdAuctionEventRecord record_click;
record_click.type = AdAuctionEventRecord::Type::kClick;
record_click.providing_origin = kViewClickProviderOrigin1;
record_click.eligible_origins = {kViewClickEligibleOrigin1};
ASSERT_TRUE(record_click.IsValid());
// Timeline as follows, in time before final time
// (d: day, w: week, h: hour, V: view, C: click, F: final time):
//
// 90d 30d 1w 1d 1h F
// 1V,1C | 2V,1C | 0V,2C | 1V,0C | 2V,2C | 1V,2C |
storage->RecordViewClick(record_view);
storage->RecordViewClick(record_click);
task_environment().FastForwardBy(base::Days(90) - base::Days(30));
storage->RecordViewClick(record_view);
storage->RecordViewClick(record_view);
storage->RecordViewClick(record_click);
task_environment().FastForwardBy(base::Days(30) - base::Days(7));
storage->RecordViewClick(record_click);
storage->RecordViewClick(record_click);
task_environment().FastForwardBy(base::Days(7) - base::Days(1));
storage->RecordViewClick(record_view);
task_environment().FastForwardBy(base::Days(1) - base::Hours(1));
storage->RecordViewClick(record_view);
storage->RecordViewClick(record_view);
storage->RecordViewClick(record_click);
storage->RecordViewClick(record_click);
task_environment().FastForwardBy(base::Hours(1));
storage->RecordViewClick(record_view);
storage->RecordViewClick(record_click);
storage->RecordViewClick(record_click);
// Fast forward one more second so that events aren't on the exact boundaries
// between time buckets. Each event will go to the next older time bucket,
// and the oldest events fall out of reporting.
task_environment().FastForwardBy(base::Seconds(1));
InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars");
g.view_and_click_counts_providers = {{kViewClickProviderOrigin1}};
g.expiry = base::Time::Now() + base::Days(90);
storage->JoinInterestGroup(g, GURL("https://joining-site.test"));
std::vector<StorageInterestGroup> groups =
storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1);
ASSERT_EQ(1u, groups.size());
blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
groups[0].bidding_browser_signals->view_and_click_counts;
EXPECT_EQ(1, view_and_click_counts->view_counts->past_hour);
EXPECT_EQ(3, view_and_click_counts->view_counts->past_day);
EXPECT_EQ(4, view_and_click_counts->view_counts->past_week);
EXPECT_EQ(4, view_and_click_counts->view_counts->past_30_days);
EXPECT_EQ(GetParam() == GroupLifetime::k90Day ? 6 : 0,
view_and_click_counts->view_counts->past_90_days);
EXPECT_EQ(2, view_and_click_counts->click_counts->past_hour);
EXPECT_EQ(4, view_and_click_counts->click_counts->past_day);
EXPECT_EQ(4, view_and_click_counts->click_counts->past_week);
EXPECT_EQ(6, view_and_click_counts->click_counts->past_30_days);
EXPECT_EQ(GetParam() == GroupLifetime::k90Day ? 7 : 0,
view_and_click_counts->click_counts->past_90_days);
}
TEST_P(InterestGroupStorageDualLifetimeTest,
ViewClickStoreRetrieve_NotEligibleOrigin) {
std::unique_ptr<InterestGroupStorage> storage = CreateStorage();
AdAuctionEventRecord record;
record.type = AdAuctionEventRecord::Type::kView;
record.providing_origin = kViewClickProviderOrigin1;
record.eligible_origins = {kViewClickEligibleOrigin2};
ASSERT_TRUE(record.IsValid());
storage->RecordViewClick(record);
InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars");
g.view_and_click_counts_providers = {{kViewClickProviderOrigin1}};
g.expiry = base::Time::Now() + base::Days(90);
storage->JoinInterestGroup(g, GURL("https://joining-site.test"));
std::vector<StorageInterestGroup> groups =
storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1);
ASSERT_EQ(1u, groups.size());
blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
groups[0].bidding_browser_signals->view_and_click_counts;
EXPECT_EQ(0, view_and_click_counts->view_counts->past_hour);
EXPECT_EQ(0, view_and_click_counts->view_counts->past_day);
EXPECT_EQ(0, view_and_click_counts->view_counts->past_week);
EXPECT_EQ(0, view_and_click_counts->view_counts->past_30_days);
EXPECT_EQ(0, view_and_click_counts->view_counts->past_90_days);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_hour);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_day);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_week);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_30_days);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_90_days);
}
TEST_P(InterestGroupStorageDualLifetimeTest,
ViewClickStoreRetrieve_ProviderMismatch) {
std::unique_ptr<InterestGroupStorage> storage = CreateStorage();
AdAuctionEventRecord record;
record.type = AdAuctionEventRecord::Type::kView;
record.providing_origin = kViewClickProviderOrigin1;
record.eligible_origins = {kViewClickEligibleOrigin1};
ASSERT_TRUE(record.IsValid());
storage->RecordViewClick(record);
InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars");
g.view_and_click_counts_providers = {{kViewClickProviderOrigin2}};
g.expiry = base::Time::Now() + base::Days(90);
storage->JoinInterestGroup(g, GURL("https://joining-site.test"));
std::vector<StorageInterestGroup> groups =
storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1);
ASSERT_EQ(1u, groups.size());
blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
groups[0].bidding_browser_signals->view_and_click_counts;
EXPECT_EQ(0, view_and_click_counts->view_counts->past_hour);
EXPECT_EQ(0, view_and_click_counts->view_counts->past_day);
EXPECT_EQ(0, view_and_click_counts->view_counts->past_week);
EXPECT_EQ(0, view_and_click_counts->view_counts->past_30_days);
EXPECT_EQ(0, view_and_click_counts->view_counts->past_90_days);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_hour);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_day);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_week);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_30_days);
EXPECT_EQ(0, view_and_click_counts->click_counts->past_90_days);
}
TEST_P(InterestGroupStorageDualLifetimeTest,
ViewClickStoreRetrieve_MultipleProviders) {
std::unique_ptr<InterestGroupStorage> storage = CreateStorage();
AdAuctionEventRecord record_provider_1;
record_provider_1.type = AdAuctionEventRecord::Type::kView;
record_provider_1.providing_origin = kViewClickProviderOrigin1;
record_provider_1.eligible_origins = {kViewClickEligibleOrigin1};
ASSERT_TRUE(record_provider_1.IsValid());
storage->RecordViewClick(record_provider_1);
AdAuctionEventRecord record_view_provider_2;
record_view_provider_2.type = AdAuctionEventRecord::Type::kView;
record_view_provider_2.providing_origin = kViewClickProviderOrigin1;
record_view_provider_2.eligible_origins = {kViewClickEligibleOrigin1};
ASSERT_TRUE(record_view_provider_2.IsValid());
AdAuctionEventRecord record_click_provider_2;
record_click_provider_2.type = AdAuctionEventRecord::Type::kClick;
record_click_provider_2.providing_origin = kViewClickProviderOrigin1;
record_click_provider_2.eligible_origins = {kViewClickEligibleOrigin1};
ASSERT_TRUE(record_click_provider_2.IsValid());
storage->RecordViewClick(record_view_provider_2);
storage->RecordViewClick(record_click_provider_2);
InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars");
g.view_and_click_counts_providers = {
{kViewClickProviderOrigin1, kViewClickProviderOrigin2}};
g.expiry = base::Time::Now() + base::Days(90);
storage->JoinInterestGroup(g, GURL("https://joining-site.test"));
std::vector<StorageInterestGroup> groups =
storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1);
ASSERT_EQ(1u, groups.size());
blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
groups[0].bidding_browser_signals->view_and_click_counts;
EXPECT_EQ(2, view_and_click_counts->view_counts->past_hour);
EXPECT_EQ(2, view_and_click_counts->view_counts->past_day);
EXPECT_EQ(2, view_and_click_counts->view_counts->past_week);
EXPECT_EQ(2, view_and_click_counts->view_counts->past_30_days);
EXPECT_EQ(GetParam() == GroupLifetime::k90Day ? 2 : 0,
view_and_click_counts->view_counts->past_90_days);
EXPECT_EQ(1, view_and_click_counts->click_counts->past_hour);
EXPECT_EQ(1, view_and_click_counts->click_counts->past_day);
EXPECT_EQ(1, view_and_click_counts->click_counts->past_week);
EXPECT_EQ(1, view_and_click_counts->click_counts->past_30_days);
EXPECT_EQ(GetParam() == GroupLifetime::k90Day ? 1 : 0,
view_and_click_counts->click_counts->past_90_days);
}
TEST_P(InterestGroupStorageDualLifetimeTest,
ViewClickStoreRetrieve_TwoEligibleOrigins) {
std::unique_ptr<InterestGroupStorage> storage = CreateStorage();
AdAuctionEventRecord record;
record.type = AdAuctionEventRecord::Type::kView;
record.providing_origin = kViewClickProviderOrigin1;
record.eligible_origins = {kViewClickEligibleOrigin1,
kViewClickEligibleOrigin2};
ASSERT_TRUE(record.IsValid());
storage->RecordViewClick(record);
InterestGroup g_cars = NewInterestGroup(kViewClickEligibleOrigin1, "cars");
g_cars.view_and_click_counts_providers = {{kViewClickProviderOrigin1}};
g_cars.expiry = base::Time::Now() + base::Days(90);
storage->JoinInterestGroup(g_cars, GURL("https://joining-site.test"));
InterestGroup g_shoes = NewInterestGroup(kViewClickEligibleOrigin2, "shoes");
g_shoes.view_and_click_counts_providers = {{kViewClickProviderOrigin1}};
g_shoes.expiry = base::Time::Now() + base::Days(90);
storage->JoinInterestGroup(g_shoes, GURL("https://joining-site.test"));
std::vector<StorageInterestGroup> groups_cars =
storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1);
ASSERT_EQ(1u, groups_cars.size());
blink::mojom::ViewAndClickCountsPtr& view_and_click_counts_cars =
groups_cars[0].bidding_browser_signals->view_and_click_counts;
EXPECT_EQ(1, view_and_click_counts_cars->view_counts->past_hour);
EXPECT_EQ(1, view_and_click_counts_cars->view_counts->past_day);
EXPECT_EQ(1, view_and_click_counts_cars->view_counts->past_week);
EXPECT_EQ(1, view_and_click_counts_cars->view_counts->past_30_days);
EXPECT_EQ(GetParam() == GroupLifetime::k90Day ? 1 : 0,
view_and_click_counts_cars->view_counts->past_90_days);
EXPECT_EQ(0, view_and_click_counts_cars->click_counts->past_hour);
EXPECT_EQ(0, view_and_click_counts_cars->click_counts->past_day);
EXPECT_EQ(0, view_and_click_counts_cars->click_counts->past_week);
EXPECT_EQ(0, view_and_click_counts_cars->click_counts->past_30_days);
EXPECT_EQ(0, view_and_click_counts_cars->click_counts->past_90_days);
std::vector<StorageInterestGroup> groups_shoes =
storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1);
ASSERT_EQ(1u, groups_shoes.size());
blink::mojom::ViewAndClickCountsPtr& view_and_click_counts_shoes =
groups_shoes[0].bidding_browser_signals->view_and_click_counts;
EXPECT_EQ(1, view_and_click_counts_shoes->view_counts->past_hour);
EXPECT_EQ(1, view_and_click_counts_shoes->view_counts->past_day);
EXPECT_EQ(1, view_and_click_counts_shoes->view_counts->past_week);
EXPECT_EQ(1, view_and_click_counts_shoes->view_counts->past_30_days);
EXPECT_EQ(GetParam() == GroupLifetime::k90Day ? 1 : 0,
view_and_click_counts_shoes->view_counts->past_90_days);
EXPECT_EQ(0, view_and_click_counts_shoes->click_counts->past_hour);
EXPECT_EQ(0, view_and_click_counts_shoes->click_counts->past_day);
EXPECT_EQ(0, view_and_click_counts_shoes->click_counts->past_week);
EXPECT_EQ(0, view_and_click_counts_shoes->click_counts->past_30_days);
EXPECT_EQ(0, view_and_click_counts_shoes->click_counts->past_90_days);
}
INSTANTIATE_TEST_SUITE_P(DualLifetime,
InterestGroupStorageDualLifetimeTest,
::testing::Values(GroupLifetime::k30Day,
GroupLifetime::k90Day));
TEST_F(InterestGroupStorageTest, StoresAllFields) {
StoresAllFieldsTest();
}
@ -3569,13 +4135,14 @@ TEST_F(InterestGroupStorageTest, UpgradeFromV31) {
TEST_F(InterestGroupStorageTest, MultiVersionUpgradeTest) {
constexpr char kMisssingFileError[] =
"You can generate the missing .sql file for the current database "
"version by running: "
"`out/Default/content_unittests "
"version by running: \n\n"
"out/Default/content_unittests "
"--gtest_filter=\"*InterestGroupStorage*Test*DumpAllIgFields\" "
"--dump-all-ig-fields` after installing sqlite3 from your package "
"manager -- you can also build the Chromium `sqlite_shell` GN "
"target and rename / symlink it on your path as sqlite3. \n\n***Make "
"sure to add the generated file to source control***.\n\n";
"--dump-all-ig-fields\n\n"
"after installing sqlite3 from your package manager -- you can also "
"build the Chromium `sqlite_shell` GN target and rename / symlink it on "
"your path as sqlite3. \n\n"
"***Make sure to add the generated file to source control***.\n\n";
for (int i = kOldestAllFieldsVersion;
i <= InterestGroupStorage::GetCurrentVersionNumberForTesting() - 1;
i++) {

@ -769,7 +769,11 @@ class BidderWorkletTest : public testing::Test {
return blink::mojom::BiddingBrowserSignals::New(
browser_signal_join_count_, browser_signal_bid_count_,
CloneWinList(browser_signal_prev_wins_),
browser_signal_for_debugging_only_in_cooldown_or_lockout_);
browser_signal_for_debugging_only_in_cooldown_or_lockout_,
/*click_and_view_counts=*/
blink::mojom::ViewAndClickCounts::New(
/*view_counts=*/blink::mojom::ViewOrClickCounts::New(),
/*click_counts=*/blink::mojom::ViewOrClickCounts::New()));
}
// Create a BidderWorklet, returning the remote. If `out_bidder_worklet_impl`

@ -7369,6 +7369,7 @@ data/interest_group/autogenSchemaV30.sql
data/interest_group/autogenSchemaV31.sql
data/interest_group/autogenSchemaV32.sql
data/interest_group/autogenSchemaV33.sql
data/interest_group/autogenSchemaV34.sql
data/interest_group/bidding_argument_validator.js
data/interest_group/bidding_argument_validator.js.mock-http-headers
data/interest_group/bidding_logic.js

@ -0,0 +1,25 @@
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE meta(key LONGVARCHAR NOT NULL UNIQUE PRIMARY KEY, value LONGVARCHAR);
INSERT INTO meta VALUES('mmap_status','-1');
INSERT INTO meta VALUES('version','34');
INSERT INTO meta VALUES('last_compatible_version','33');
CREATE TABLE interest_groups(expiration INTEGER NOT NULL,last_updated INTEGER NOT NULL,next_update_after INTEGER NOT NULL,owner TEXT NOT NULL,joining_origin TEXT NOT NULL,exact_join_time INTEGER NOT NULL,name TEXT NOT NULL,priority DOUBLE NOT NULL,enable_bidding_signals_prioritization INTEGER NOT NULL,priority_vector TEXT NOT NULL,priority_signals_overrides TEXT NOT NULL,seller_capabilities TEXT NOT NULL,all_sellers_capabilities INTEGER NOT NULL,execution_mode INTEGER NOT NULL,joining_url TEXT NOT NULL,bidding_url TEXT NOT NULL,bidding_wasm_helper_url TEXT NOT NULL,update_url TEXT NOT NULL,trusted_bidding_signals_url TEXT NOT NULL,trusted_bidding_signals_keys TEXT NOT NULL,trusted_bidding_signals_slot_size_mode INTEGER NOT NULL,max_trusted_bidding_signals_url_length INTEGER NOT NULL,trusted_bidding_signals_coordinator TEXT,view_and_click_counts_providers TEXT,user_bidding_signals TEXT,ads_pb BLOB NOT NULL,ad_components_pb BLOB NOT NULL,ad_sizes TEXT NOT NULL,size_groups TEXT NOT NULL,auction_server_request_flags INTEGER NOT NULL,additional_bid_key BLOB NOT NULL,aggregation_coordinator_origin TEXT,storage_size INTEGER NOT NULL,last_k_anon_updated_time INTEGER NOT NULL,kanon_keys BLOB NOT NULL,PRIMARY KEY(owner,name));
INSERT INTO interest_groups VALUES(11649590195539000,11646998195539000,-9223372036854775808,'https://full.example.com','https://full.example.com',11646998195539000,'full',1.0,1,'{"a":2.0,"b":-2.2}','{"a":-2.0,"c":10.0,"d":1.2}','{"https://full.example.com":"1","https://partial.example.com":"2"}',3,2,'https://full.example.com/','https://full.example.com/bid','https://full.example.com/bid_wasm','https://full.example.com/update','https://full.example.com/signals','["a","b","c","d"]',2,8000,'https://coordinator.test',replace('\n+https://view-and-click-counts-provider.test','\n',char(10)),'foo',X'd401f0580a92010a1c68747470733a2f2f66756c6c2e6578616d706c652e636f6d2f616431120767726f75705f311a096d6574616461746131220862757965725f69642a097368617265645f6964320a616452656e64657249643a15680d53207265706f7274696e6701503c420e73656c65637461626c655f6964313a100034324a067363616e20310a3d0a1c680d410066469400003211940c321a096d0d942c32220962757965725f696432',X'9101f04c0a490a2568747470733a2f2f66756c6c2e6578616d706c652e636f6d2f6164636f6d706f6e656e7431120767726f75705f311a0a6d657461646174613163320b616452656e6465724964320a44964b000032114b04321a154b2432634a067363616e2032','{"size_1":"{\"height\":150.0,\"height_units\":1,\"width\":300.0,\"width_units\":1}","size_2":"{\"height\":480.0,\"height_units\":1,\"width\":640.0,\"width_units\":1}","size_3":"{\"height\":100.0,\"height_units\":2,\"width\":100.0,\"width_units\":2}"}','{"group_1":"[\"size_1\"]","group_2":"[\"size_1\",\"size_2\"]","group_3":"[\"size_3\"]"}',3,X'','https://coordinator.test',822,-9223372036854775808,X'');
CREATE TABLE joined_k_anon(hashed_key BLOB NOT NULL,last_reported_to_anon_server_time INTEGER NOT NULL,PRIMARY KEY(hashed_key));
CREATE TABLE join_history(owner TEXT NOT NULL,name TEXT NOT NULL,join_time INTEGER NOT NULL,count INTEGER NOT NULL,PRIMARY KEY(owner, name, join_time) FOREIGN KEY(owner,name) REFERENCES interest_groups);
INSERT INTO join_history VALUES('https://full.example.com','full',11646979200000000,1);
CREATE TABLE bid_history(owner TEXT NOT NULL,name TEXT NOT NULL,bid_time INTEGER NOT NULL,count INTEGER NOT NULL,PRIMARY KEY(owner, name, bid_time) FOREIGN KEY(owner,name) REFERENCES interest_groups);
CREATE TABLE win_history(owner TEXT NOT NULL,name TEXT NOT NULL,win_time INTEGER NOT NULL,ad TEXT NOT NULL,FOREIGN KEY(owner,name) REFERENCES interest_groups);
CREATE TABLE lockout_debugging_only_report(id INTEGER NOT NULL,starting_time INTEGER NOT NULL,duration INTEGER NOT NULL,PRIMARY KEY(id));
CREATE TABLE cooldown_debugging_only_report(origin TEXT NOT NULL,starting_time INTEGER NOT NULL,type INTEGER NOT NULL,PRIMARY KEY(origin));
CREATE TABLE bidding_and_auction_server_keys(coordinator TEXT NOT NULL,keys BLOB NOT NULL,expiration INTEGER NOT NULL,PRIMARY KEY(coordinator));
CREATE TABLE view_and_click_events(provider_origin STRING NOT NULL,eligible_origin STRING NOT NULL,uncompacted_view_events STRING NOT NULL,compacted_view_events STRING NOT NULL,uncompacted_click_events STRING NOT NULL,compacted_click_events STRING NOT NULL,PRIMARY KEY(provider_origin, eligible_origin));
CREATE INDEX interest_group_expiration ON interest_groups(expiration DESC, owner, name);
CREATE INDEX interest_group_owner ON interest_groups(owner,expiration DESC,next_update_after ASC,name);
CREATE INDEX interest_group_owner_and_type ON interest_groups(LENGTH(additional_bid_key) == 0,owner,expiration DESC,name);
CREATE INDEX interest_group_owner_size ON interest_groups(owner,expiration DESC,storage_size);
CREATE INDEX interest_group_joining_origin ON interest_groups(joining_origin, expiration DESC, owner, name);
CREATE INDEX k_anon_last_server_time_idx ON joined_k_anon(last_reported_to_anon_server_time DESC);
CREATE INDEX win_history_index ON win_history(owner,name,win_time DESC);
COMMIT;

@ -221,6 +221,42 @@ struct PreviousWin {
string ad_json;
};
// Used by ClickAndViewCounts -- both view and click counts share the same time
// bucketing scheme.
//
// There can be some imprecison due to the fact that counts older than one hour
// get compacted by DB maintenance to the nearest hour, to save storage.
//
// Note that the longer time intervals include all events in the shorter time
// intervals. For example, every event in the past hour is also an event in the
// past day, etc.
struct ViewOrClickCounts {
// Views or clicks within the past hour.
int32 past_hour = 0;
// Views or clicks within the past day.
int32 past_day = 0;
// Views or clicks within the past week.
int32 past_week = 0;
// Views or clicks within the past 30 days.
int32 past_30_days = 0;
// Views or clicks within the past 90 days.
int32 past_90_days = 0;
};
// A part of BiddingBrowserSignals that provides counts of views and clicks for
// the given buyer origin (the "eligible origin") over various time buckets.
struct ViewAndClickCounts {
// Counts for view events for the given buyer origin.
ViewOrClickCounts view_counts;
// Counts for click events for the given buyer origin.
ViewOrClickCounts click_counts;
};
// Browser signals passed to the BidderWorklet's generateBid() method that are
// stored on disk and updated by the browser, as opposed to coming from the
// frame running the auction, or from the definition of the InterestGroup taking
@ -239,6 +275,10 @@ struct BiddingBrowserSignals {
// Whether the browser is under lockout or the buyer's origin is under
// cooldown for sending forDebuggingOnly reports.
bool for_debugging_only_in_cooldown_or_lockout;
// Loaded time-bucketed view and click counts aggregated across the
// `view_and_click_counts_providers` for this interest group.
ViewAndClickCounts view_and_click_counts;
};
// Represents an interest group with associated metadata. This struct is

@ -1921,7 +1921,11 @@ TEST_F(SharedStorageWorkletTest, InterestGroups) {
blink::mojom::BiddingBrowserSignals::New(
/*join_count=*/1,
/*bid_count=*/2, std::move(prev_wins),
/*for_debugging_only_in_cooldown_or_lockout=*/false);
/*for_debugging_only_in_cooldown_or_lockout=*/false,
/*click_and_view_counts=*/
blink::mojom::ViewAndClickCounts::New(
/*view_counts=*/blink::mojom::ViewOrClickCounts::New(),
/*click_counts=*/blink::mojom::ViewOrClickCounts::New()));
blink::InterestGroup ig;
ig.expiry = now + base::Seconds(3000);

@ -28,6 +28,7 @@ chromium-metrics-reviews@google.com.
summary="BiddingAndAuctionServerKeyProtos"/>
<variant name="KAnonKeyProtos" summary="KAnonKeyProtos"/>
<variant name="ListOfOrigins" summary="ListOfOrigins"/>
<variant name="ListOfTimestamps" summary="ListOfTimestamps"/>
</variants>
<histogram name="API.EffectiveStorageAccess.AllowedByStorageAccessType"