Commit 061f86e4 authored by John Delaney's avatar John Delaney Committed by Commit Bot

Implement sqlite storage layer for conversion measurement API

This change implements a storage layer in sqlite for persisting
impressions and conversions from the API to disk.

The storage for the API consists of two tables, an impression table and
a conversion table. Every row in the conversion table is associated with
an impression in the impression table to reuse common data such as
conversion and reporting origins.

The storage layer does not implement specific functionality like last
clicked attribution, bit limits, or reporting delays. Instead it abstracts
that functionality to an injectable delegate class.

This change also includes some common structs that are used by the API,
such as a new mojo struct blink::mojom::Conversion.

The functionality of the API is described on
https://github.com/csharrison/conversion-measurement-api

Reference prototype change:
https://chromium-review.googlesource.com/c/chromium/src/+/1967220

Bug: 1014604
Change-Id: Icff2d720fe7c595398835fcfd026f6d321cb9c99
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1965450
Commit-Queue: John Delaney <johnidel@chromium.org>
Reviewed-by: default avatarVictor Costan <pwnall@chromium.org>
Reviewed-by: default avatarCharlie Harrison <csharrison@chromium.org>
Reviewed-by: default avatarJohn Abd-El-Malek <jam@chromium.org>
Cr-Commit-Position: refs/heads/master@{#735418}
parent 0d581e87
...@@ -671,6 +671,15 @@ jumbo_source_set("browser") { ...@@ -671,6 +671,15 @@ jumbo_source_set("browser") {
"content_index/content_index_service_impl.h", "content_index/content_index_service_impl.h",
"content_service_delegate_impl.cc", "content_service_delegate_impl.cc",
"content_service_delegate_impl.h", "content_service_delegate_impl.h",
"conversions/conversion_report.cc",
"conversions/conversion_report.h",
"conversions/conversion_storage.h",
"conversions/conversion_storage_sql.cc",
"conversions/conversion_storage_sql.h",
"conversions/storable_conversion.cc",
"conversions/storable_conversion.h",
"conversions/storable_impression.cc",
"conversions/storable_impression.h",
"cookie_store/cookie_change_subscription.cc", "cookie_store/cookie_change_subscription.cc",
"cookie_store/cookie_change_subscription.h", "cookie_store/cookie_change_subscription.h",
"cookie_store/cookie_store_context.cc", "cookie_store/cookie_store_context.cc",
......
csharrison@chromium.org
johnidel@chromium.org
# TEAM: privacy-sandbox-dev@chromium.org
# TODO(https://crbug.com/1045468): Add a component for ConversionMeasurement.
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/conversions/conversion_report.h"
#include <tuple>
namespace content {
ConversionReport::ConversionReport(const StorableImpression& impression,
const std::string& conversion_data,
base::Time report_time,
const base::Optional<int64_t>& conversion_id)
: impression(impression),
conversion_data(conversion_data),
report_time(report_time),
conversion_id(conversion_id) {}
ConversionReport::ConversionReport(const ConversionReport& other) = default;
ConversionReport::~ConversionReport() = default;
std::ostream& operator<<(std::ostream& out, const ConversionReport& report) {
out << "impression_data: " << report.impression.impression_data()
<< ", impression_origin: " << report.impression.impression_origin()
<< ", conversion_origin: " << report.impression.conversion_origin()
<< ", reporting_origin: " << report.impression.reporting_origin()
<< ", conversion_data: " << report.conversion_data
<< ", report_time: " << report.report_time
<< ", attribution_credit: " << report.attribution_credit;
return out;
}
} // namespace content
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CONTENT_BROWSER_CONVERSIONS_CONVERSION_REPORT_H_
#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_REPORT_H_
#include <stdint.h>
#include <string>
#include <vector>
#include "base/optional.h"
#include "base/time/time.h"
#include "content/browser/conversions/storable_impression.h"
#include "content/common/content_export.h"
namespace content {
// Struct that contains all the data needed to serialize and send a conversion
// report. This represents the report for a conversion event and its associated
// impression.
struct CONTENT_EXPORT ConversionReport {
// The conversion_id may not be set for a conversion report.
ConversionReport(const StorableImpression& impression,
const std::string& conversion_data,
base::Time report_time,
const base::Optional<int64_t>& conversion_id);
ConversionReport(const ConversionReport& other);
~ConversionReport();
// Impression associated with this conversion report.
const StorableImpression impression;
// Data provided at reporting time by the reporting origin. String
// representing a valid hexadecimal number.
const std::string conversion_data;
// The time this conversion report should be sent.
base::Time report_time;
// The attribution credit assigned to this conversion report. This is derived
// from the set of all impressions that matched a singular conversion event.
// This should be in the range 0-100. A set of ConversionReports for one
// conversion event should have their |attribution_credit| sum equal to 100.
int attribution_credit = 0;
// Id assigned by storage to uniquely identify a completed conversion. If
// null, an ID has not been assigned yet.
const base::Optional<int64_t> conversion_id;
};
// Only used for logging.
CONTENT_EXPORT std::ostream& operator<<(
std::ostream& out,
const ConversionReport& ConversionReport);
} // namespace content
#endif // CONTENT_BROWSER_CONVERSIONS_CONVERSION_REPORT_H_
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CONTENT_BROWSER_CONVERSIONS_CONVERSION_STORAGE_H_
#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_STORAGE_H_
#include <stdint.h>
#include <vector>
#include "base/time/time.h"
#include "content/browser/conversions/conversion_report.h"
#include "content/browser/conversions/storable_conversion.h"
#include "content/browser/conversions/storable_impression.h"
namespace content {
// This class provides an interface for persisting impression/conversion data to
// disk, and performing queries on it.
class ConversionStorage {
public:
// Storage delegate that can supplied to extend basic conversion storage
// functionality like annotating conversion reports.
class Delegate {
public:
// New conversions will be sent through this callback for
// pruning/modification before they are added to storage. This will be
// called during the execution of
// ConversionStorage::MaybeCreateAndStoreConversionReports(). |reports| will
// contain a report for each matching impression for a given conversion
// event. Each report will be pre-populated from storage with the conversion
// event data.
virtual void ProcessNewConversionReports(
std::vector<ConversionReport>* reports) = 0;
// This limit is used to determine if an impression is allowed to schedule
// a new conversion reports. When an impression reaches this limit it is
// marked inactive and no new conversion reports will be created for it.
// Impressions will be checked against this limit after they schedule a new
// report.
virtual int GetMaxConversionsPerImpression() const = 0;
};
virtual ~ConversionStorage() = default;
// Initializes the storage. Returns true on success, otherwise the storage
// should not be used.
virtual bool Initialize() = 0;
// Add |impression| to storage. Two impressions are considered
// matching when they share a <reporting_origin, conversion_origin> pair. When
// an impression is stored, all matching impressions that have
// already converted are marked as inactive, and are no longer eligible for
// reporting. Unconverted matching impressions are not modified.
virtual void StoreImpression(const StorableImpression& impression) = 0;
// Finds all stored impressions matching a given |conversion|, and stores new
// associated conversion reports. The delegate will receive a call
// to Delegate::ProcessNewConversionReports() before the reports are added to
// storage. Only active impressions will receive new conversions. Returns the
// number of new conversion reports that have been scheduled/added to storage.
virtual int MaybeCreateAndStoreConversionReports(
const StorableConversion& conversion) = 0;
// Returns all of the conversion reports that should be sent before
// |max_report_time|. This call is logically const, and does not modify the
// underlying storage.
virtual std::vector<ConversionReport> GetConversionsToReport(
base::Time max_report_time) = 0;
// Deletes all impressions that have expired and have no pending conversion
// reports. Returns the number of impressions that were deleted.
virtual int DeleteExpiredImpressions() = 0;
// Deletes the conversion report with the given |conversion_id|. Returns
// whether the deletion was successful.
virtual bool DeleteConversion(int64_t conversion_id) = 0;
// TODO(johnidel): Add an API to ConversionStorage that removes site data, and
// hook it into the data remover. This should be added before the API is
// enabled.
};
} // namespace content
#endif // CONTENT_BROWSER_CONVERSIONS_CONVERSION_STORAGE_H_
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/conversions/conversion_storage_sql.h"
#include <string>
#include <utility>
#include "base/bind.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/optional.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "sql/recovery.h"
#include "sql/statement.h"
#include "sql/transaction.h"
#include "url/gurl.h"
#include "url/origin.h"
#include "url/url_constants.h"
namespace content {
namespace {
const base::FilePath::CharType kDatabaseName[] =
FILE_PATH_LITERAL("Conversions");
std::string SerializeOrigin(const url::Origin& origin) {
// Conversion API is only designed to be used for secure contexts (targets and
// reporting endpoints). We should have filtered out bad origins at a higher
// layer.
//
// Because we only allow https origins to use the API, we could potentially
// omit the scheme from storage to save 8 bytes per origin. However this would
// require maintaining our own serialization logic and also complicates
// extending storage to other scheme in the future.
DCHECK(!origin.opaque());
DCHECK_EQ(url::kHttpsScheme, origin.scheme());
return origin.Serialize();
}
url::Origin DeserializeOrigin(const std::string& origin) {
return url::Origin::Create(GURL(origin));
}
int64_t SerializeTime(base::Time time) {
return time.ToDeltaSinceWindowsEpoch().InMicroseconds();
}
base::Time DeserializeTime(int64_t microseconds) {
return base::Time::FromDeltaSinceWindowsEpoch(
base::TimeDelta::FromMicroseconds(microseconds));
}
} // namespace
ConversionStorageSql::ConversionStorageSql(
const base::FilePath& path_to_database_dir,
Delegate* delegate,
base::Clock* clock)
: path_to_database_(path_to_database_dir.Append(kDatabaseName)),
clock_(clock),
delegate_(delegate),
weak_factory_(this) {
DETACH_FROM_SEQUENCE(sequence_checker_);
}
ConversionStorageSql::~ConversionStorageSql() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
bool ConversionStorageSql::Initialize() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
db_.set_histogram_tag("Conversions");
// Supply this callback with a weak_ptr to avoid calling the error callback
// after |this| has been deleted.
db_.set_error_callback(
base::BindRepeating(&ConversionStorageSql::DatabaseErrorCallback,
weak_factory_.GetWeakPtr()));
db_.set_page_size(4096);
db_.set_cache_size(32);
db_.set_exclusive_locking();
return db_.Open(path_to_database_) && InitializeSchema();
}
void ConversionStorageSql::StoreImpression(
const StorableImpression& impression) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Wrap the deactivation and insertion in the same transaction. If the
// deactivation fails, we do not want to store the new impression as we may
// return the wrong set of impressions for a conversion.
sql::Transaction transaction(&db_);
if (!transaction.Begin())
return;
// In the case where we get a new impression for a given <reporting_origin,
// conversion_origin> we should mark all active, converted impressions with
// the matching <reporting_origin, conversion_origin> as not active.
const char kDeactivateMatchingConvertedImpressionsSql[] =
"UPDATE impressions SET active = 0 "
"WHERE conversion_origin = ? AND reporting_origin = ? AND "
"active = 1 AND num_conversions > 0";
sql::Statement deactivate_statement(db_.GetCachedStatement(
SQL_FROM_HERE, kDeactivateMatchingConvertedImpressionsSql));
deactivate_statement.BindString(
0, SerializeOrigin(impression.conversion_origin()));
deactivate_statement.BindString(
1, SerializeOrigin(impression.reporting_origin()));
deactivate_statement.Run();
const char kInsertImpressionSql[] =
"INSERT INTO impressions"
"(impression_data, impression_origin, conversion_origin, "
"reporting_origin, impression_time, expiry_time) "
"VALUES (?,?,?,?,?,?)";
sql::Statement statement(
db_.GetCachedStatement(SQL_FROM_HERE, kInsertImpressionSql));
statement.BindString(0, impression.impression_data());
statement.BindString(1, SerializeOrigin(impression.impression_origin()));
statement.BindString(2, SerializeOrigin(impression.conversion_origin()));
statement.BindString(3, SerializeOrigin(impression.reporting_origin()));
statement.BindInt64(4, SerializeTime(impression.impression_time()));
statement.BindInt64(5, SerializeTime(impression.expiry_time()));
statement.Run();
transaction.Commit();
}
int ConversionStorageSql::MaybeCreateAndStoreConversionReports(
const StorableConversion& conversion) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
const url::Origin& conversion_origin = conversion.conversion_origin();
const url::Origin& reporting_origin = conversion.reporting_origin();
base::Time current_time = clock_->Now();
int64_t serialized_current_time = SerializeTime(current_time);
// Get all impressions that match this <reporting_origin, conversion_origin>
// pair. Only get impressions that are active and not past their expiry time.
const char kGetMatchingImpressionsSql[] =
"SELECT impression_id, impression_data, impression_origin, "
"impression_time, expiry_time "
"FROM impressions WHERE conversion_origin = ? AND reporting_origin = ? "
"AND active = 1 AND expiry_time > ? "
"ORDER BY impression_time DESC";
sql::Statement statement(
db_.GetCachedStatement(SQL_FROM_HERE, kGetMatchingImpressionsSql));
statement.BindString(0, SerializeOrigin(conversion_origin));
statement.BindString(1, SerializeOrigin(reporting_origin));
statement.BindInt64(2, serialized_current_time);
// Create a set of default reports to add to storage.
std::vector<ConversionReport> new_reports;
while (statement.Step()) {
int64_t impression_id = statement.ColumnInt64(0);
std::string impression_data = statement.ColumnString(1);
url::Origin impression_origin =
DeserializeOrigin(statement.ColumnString(2));
base::Time impression_time = DeserializeTime(statement.ColumnInt64(3));
base::Time expiry_time = DeserializeTime(statement.ColumnInt64(4));
StorableImpression impression(impression_data, impression_origin,
conversion_origin, reporting_origin,
impression_time, expiry_time, impression_id);
ConversionReport report(std::move(impression), conversion.conversion_data(),
current_time, /*conversion_id=*/base::nullopt);
new_reports.push_back(std::move(report));
}
// Exit early if the last statement wasn't valid or if we have no new reports.
if (!statement.Succeeded() || new_reports.empty())
return 0;
// Allow the delegate to make arbitrary changes to the new conversion reports
// before we add them storage.
delegate_->ProcessNewConversionReports(&new_reports);
// |delegate_| may have removed all reports at this point.
if (new_reports.empty())
return 0;
sql::Transaction transaction(&db_);
if (!transaction.Begin())
return 0;
const char kStoreConversionSql[] =
"INSERT INTO conversions "
"(impression_id, conversion_data, conversion_time, report_time, "
"attribution_credit) VALUES(?,?,?,?,?)";
sql::Statement store_conversion_statement(
db_.GetCachedStatement(SQL_FROM_HERE, kStoreConversionSql));
// Mark impressions inactive if they hit the max conversions allowed limit
// supplied by the delegate. Because only active impressions log conversions,
// we do not need to handle cases where active = 0 in this query. Update
// statements atomically update all values at once. Therefore, for the check
// |num_conversions < ?|, we used the max number of conversions - 1 as the
// param. This is not done inside the query to generate better opcodes.
const char kUpdateImpressionForConversionSql[] =
"UPDATE impressions SET num_conversions = num_conversions + 1, "
"active = num_conversions < ? "
"WHERE impression_id = ?";
sql::Statement impression_update_statement(
db_.GetCachedStatement(SQL_FROM_HERE, kUpdateImpressionForConversionSql));
// Subtract one from the max number of conversions per the query comment
// above. We need to account for the new conversion in this comparison so we
// provide the max number of conversions prior to this new conversion being
// logged.
int max_prior_conversions_before_inactive =
delegate_->GetMaxConversionsPerImpression() - 1;
for (const ConversionReport& report : new_reports) {
// Insert each report into the conversions table.
store_conversion_statement.Reset(/*clear_bound_vars=*/true);
store_conversion_statement.BindInt64(0, *report.impression.impression_id());
store_conversion_statement.BindString(1, report.conversion_data);
store_conversion_statement.BindInt64(2, serialized_current_time);
store_conversion_statement.BindInt64(3, SerializeTime(report.report_time));
store_conversion_statement.BindInt(4, report.attribution_credit);
store_conversion_statement.Run();
// Update each associated impression.
impression_update_statement.Reset(/*clear_bound_vars=*/true);
impression_update_statement.BindInt(0,
max_prior_conversions_before_inactive);
impression_update_statement.BindInt64(1,
*report.impression.impression_id());
impression_update_statement.Run();
}
if (!transaction.Commit())
return 0;
return new_reports.size();
}
std::vector<ConversionReport> ConversionStorageSql::GetConversionsToReport(
base::Time max_report_time) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Get all entries in the conversions table with a |report_time| less than
// |expired_at| and their matching information from the impression table.
const char kGetExpiredConversionsSql[] =
"SELECT C.conversion_data, C.attribution_credit, C.report_time, "
"C.conversion_id, I.impression_origin, I.conversion_origin, "
"I.reporting_origin, I.impression_data, I.impression_time, "
"I.expiry_time, I.impression_id "
"FROM conversions C JOIN impressions I ON "
"C.impression_id = I.impression_id WHERE C.report_time <= ?";
sql::Statement statement(
db_.GetCachedStatement(SQL_FROM_HERE, kGetExpiredConversionsSql));
statement.BindInt64(0, SerializeTime(max_report_time));
std::vector<ConversionReport> conversions;
while (statement.Step()) {
std::string conversion_data = statement.ColumnString(0);
int attribution_credit = statement.ColumnInt(1);
base::Time report_time = DeserializeTime(statement.ColumnInt64(2));
int64_t conversion_id = statement.ColumnInt64(3);
url::Origin impression_origin =
DeserializeOrigin(statement.ColumnString(4));
url::Origin conversion_origin =
DeserializeOrigin(statement.ColumnString(5));
url::Origin reporting_origin = DeserializeOrigin(statement.ColumnString(6));
std::string impression_data = statement.ColumnString(7);
base::Time impression_time = DeserializeTime(statement.ColumnInt64(8));
base::Time expiry_time = DeserializeTime(statement.ColumnInt64(9));
int64_t impression_id = statement.ColumnInt64(10);
// Create the impression and ConversionReport objects from the retrieved
// columns.
StorableImpression impression(impression_data, impression_origin,
conversion_origin, reporting_origin,
impression_time, expiry_time, impression_id);
ConversionReport report(std::move(impression), conversion_data, report_time,
conversion_id);
report.attribution_credit = attribution_credit;
conversions.push_back(std::move(report));
}
if (!statement.Succeeded())
return {};
return conversions;
}
int ConversionStorageSql::DeleteExpiredImpressions() {
// Delete all impressions that have no associated conversions and are past
// their expiry time. Optimized by |kImpressionExpiryIndexSql|.
const char kDeleteExpiredImpressionsSql[] =
"DELETE FROM impressions WHERE expiry_time <= ? AND "
"impression_id NOT IN (SELECT impression_id FROM conversions)";
sql::Statement delete_expired_statement(
db_.GetCachedStatement(SQL_FROM_HERE, kDeleteExpiredImpressionsSql));
delete_expired_statement.BindInt64(0, SerializeTime(clock_->Now()));
if (!delete_expired_statement.Run())
return 0;
int change_count = db_.GetLastChangeCount();
// Delete all impressions that have no associated conversions and are
// inactive. This is done in a separate statement from
// |kDeleteExpiredImpressionsSql| so that each query is optimized by an index.
// Optimized by |kConversionUrlIndexSql|.
const char kDeleteInactiveImpressionsSql[] =
"DELETE FROM impressions WHERE active = 0 AND "
"impression_id NOT IN (SELECT impression_id FROM conversions)";
sql::Statement delete_inactive_statement(
db_.GetCachedStatement(SQL_FROM_HERE, kDeleteInactiveImpressionsSql));
if (!delete_inactive_statement.Run())
return change_count;
return change_count + db_.GetLastChangeCount();
}
bool ConversionStorageSql::DeleteConversion(int64_t conversion_id) {
// Delete the row identified by |conversion_id|.
const char kDeleteSentConversionSql[] =
"DELETE FROM conversions WHERE conversion_id = ?";
sql::Statement statement(
db_.GetCachedStatement(SQL_FROM_HERE, kDeleteSentConversionSql));
statement.BindInt64(0, conversion_id);
if (!statement.Run())
return false;
DCHECK_EQ(1, db_.GetLastChangeCount());
return db_.GetLastChangeCount() > 0;
}
bool ConversionStorageSql::InitializeSchema() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(johnidel, csharrison): Many impressions will share a target origin and
// a reporting origin, so it makes sense to make a "shared string" table for
// these to save disk / memory. However, this complicates the schema a lot, so
// probably best to only do it if there's performance problems here.
//
// Origins usually aren't _that_ big compared to a 64 bit integer(8 bytes).
//
// All of the columns in this table are designed to be "const" except for
// |num_conversions| and |active| which are updated when a new conversion is
// received. |num_conversions| is the number of times a conversion report has
// been created for a given impression. |delegate_| can choose to enforce a
// maximum limit on this. |active| indicates whether an impression is able to
// create new associated conversion reports. |active| can be unset on a number
// of conditions:
// - An impression converted too many times.
// - A new impression was stored after an impression converted, making it
// ineligible for new impressions due to the attribution model documented
// in StoreImpression().
// - An impression has expired but still has unsent conversions in the
// conversions table meaning it cannot be deleted yet.
const char kImpressionTableSql[] =
"CREATE TABLE IF NOT EXISTS impressions "
"(impression_id INTEGER PRIMARY KEY,"
" impression_data TEXT NOT NULL,"
" impression_origin TEXT NOT NULL,"
" conversion_origin TEXT NOT NULL,"
" reporting_origin TEXT NOT NULL,"
" impression_time INTEGER NOT NULL,"
" expiry_time INTEGER NOT NULL,"
" num_conversions INTEGER DEFAULT 0,"
" active INTEGER DEFAULT 1)";
if (!db_.Execute(kImpressionTableSql))
return false;
// Optimizes impression lookup by conversion/reporting origin during calls to
// MaybeCreateAndStoreConversionReports(), StoreImpression(),
// DeleteExpiredImpressions(). Impressions and conversions are considered
// matching if they share this pair. These calls only look at active
// conversions, so include |active| in the index.
const char kConversionUrlIndexSql[] =
"CREATE INDEX IF NOT EXISTS conversion_origin_idx "
"ON impressions(active, conversion_origin, reporting_origin)";
if (!db_.Execute(kConversionUrlIndexSql))
return false;
// Optimizes calls to DeleteExpiredImpressions() and
// MaybeCreateAndStoreConversionReports() by indexing impressions by expiry
// time. Both calls require only returning impressions that expire after a
// given time.
const char kImpressionExpiryIndexSql[] =
"CREATE INDEX IF NOT EXISTS impression_expiry_idx "
"ON impressions(expiry_time)";
if (!db_.Execute(kImpressionExpiryIndexSql))
return false;
// All columns in this table are const. |impression_id| is the primary key of
// a row in the [impressions] table, [impressions.impression_id].
// |conversion_time| is the time at which the conversion was registered, and
// should be used for clearing site data. |report_time| is the time a
// <conversion, impression> pair should be reported, and is specified by
// |delegate_|. |attribution_credit| is assigned by |delegate_| based on the
// set of impressions returned from |kGetMatchingImpressionsSql|.
const char kConversionTableSql[] =
"CREATE TABLE IF NOT EXISTS conversions "
"(conversion_id INTEGER PRIMARY KEY,"
" impression_id INTEGER,"
" conversion_data TEXT NOT NULL,"
" conversion_time INTEGER NOT NULL,"
" report_time INTEGER NOT NULL,"
" attribution_credit INTEGER NOT NULL)";
if (!db_.Execute(kConversionTableSql))
return false;
// Optimize sorting conversions by report time for calls to
// GetConversionsToReport(). The reports with the earliest report times are
// periodically fetched from storage to be sent.
const char kConversionReportTimeIndexSql[] =
"CREATE INDEX IF NOT EXISTS conversion_report_idx "
"ON conversions(report_time)";
if (!db_.Execute(kConversionReportTimeIndexSql))
return false;
// Want to optimize conversion look up by click id. This allows us to
// quickly know if an expired impression can be deleted safely if it has no
// corresponding pending conversions during calls to
// DeleteExpiredImpressions().
const char kConversionClickIdIndexSql[] =
"CREATE INDEX IF NOT EXISTS conversion_impression_id_idx "
"ON conversions(impression_id)";
return db_.Execute(kConversionClickIdIndexSql);
}
void ConversionStorageSql::DatabaseErrorCallback(int extended_error,
sql::Statement* stmt) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Attempt to recover corrupt databases.
if (sql::Recovery::ShouldRecover(extended_error)) {
// Prevent reentrant calls.
db_.reset_error_callback();
// After this call, the |db_| handle is poisoned so that future calls will
// return errors until the handle is re-opened.
sql::Recovery::RecoverDatabase(&db_, path_to_database_);
// The DLOG(FATAL) below is intended to draw immediate attention to errors
// in newly-written code. Database corruption is generally a result of OS
// or hardware issues, not coding errors at the client level, so displaying
// the error would probably lead to confusion. The ignored call signals the
// test-expectation framework that the error was handled.
ignore_result(sql::Database::IsExpectedSqliteError(extended_error));
return;
}
// The default handling is to assert on debug and to ignore on release.
if (!sql::Database::IsExpectedSqliteError(extended_error))
DLOG(FATAL) << db_.GetErrorMessage();
}
} // namespace content
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CONTENT_BROWSER_CONVERSIONS_CONVERSION_STORAGE_SQL_H_
#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_STORAGE_SQL_H_
#include "base/files/file_path.h"
#include "base/memory/weak_ptr.h"
#include "base/sequence_checker.h"
#include "base/time/clock.h"
#include "content/browser/conversions/conversion_report.h"
#include "content/browser/conversions/conversion_storage.h"
#include "content/common/content_export.h"
#include "sql/database.h"
namespace base {
class Clock;
} // namespace base
namespace content {
// Provides an implementation of ConversionStorage that is backed by SQLite.
// This class may be constructed on any sequence but must be accessed and
// destroyed on the same sequence. The sequence must outlive |this|.
class CONTENT_EXPORT ConversionStorageSql : public ConversionStorage {
public:
ConversionStorageSql(const base::FilePath& path_to_database_dir,
Delegate* delegate,
base::Clock* clock);
ConversionStorageSql(const ConversionStorageSql& other) = delete;
ConversionStorageSql& operator=(const ConversionStorageSql& other) = delete;
~ConversionStorageSql() override;
private:
// ConversionStorage
bool Initialize() override;
void StoreImpression(const StorableImpression& impression) override;
int MaybeCreateAndStoreConversionReports(
const StorableConversion& conversion) override;
std::vector<ConversionReport> GetConversionsToReport(
base::Time expiry_time) override;
int DeleteExpiredImpressions() override;
bool DeleteConversion(int64_t conversion_id) override;
bool InitializeSchema();
void DatabaseErrorCallback(int extended_error, sql::Statement* stmt);
const base::FilePath path_to_database_;
sql::Database db_;
// Must outlive |this|.
base::Clock* const clock_;
// Must outlive |this|.
Delegate* const delegate_;
SEQUENCE_CHECKER(sequence_checker_);
base::WeakPtrFactory<ConversionStorageSql> weak_factory_;
};
} // namespace content
#endif // CONTENT_BROWSER_CONVERSIONS_CONVERSION_STORAGE_SQL_H_
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/conversions/conversion_storage_sql.h"
#include <memory>
#include "base/files/scoped_temp_dir.h"
#include "base/run_loop.h"
#include "base/test/simple_test_clock.h"
#include "content/browser/conversions/conversion_report.h"
#include "content/browser/conversions/conversion_test_utils.h"
#include "content/browser/conversions/storable_conversion.h"
#include "content/browser/conversions/storable_impression.h"
#include "sql/database.h"
#include "sql/test/scoped_error_expecter.h"
#include "sql/test/test_helpers.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace content {
class ConversionStorageSqlTest : public testing::Test {
public:
ConversionStorageSqlTest() = default;
void SetUp() override { ASSERT_TRUE(temp_directory_.CreateUniqueTempDir()); }
void OpenDatabase() {
storage_.reset();
storage_ = std::make_unique<ConversionStorageSql>(temp_directory_.GetPath(),
&delegate_, &clock_);
EXPECT_TRUE(storage_->Initialize());
}
void CloseDatabase() { storage_.reset(); }
void AddReportToStorage() {
storage_->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
storage_->MaybeCreateAndStoreConversionReports(DefaultConversion());
}
base::FilePath db_path() {
return temp_directory_.GetPath().Append(FILE_PATH_LITERAL("Conversions"));
}
base::SimpleTestClock* clock() { return &clock_; }
ConversionStorage* storage() { return storage_.get(); }
private:
base::ScopedTempDir temp_directory_;
std::unique_ptr<ConversionStorage> storage_;
base::SimpleTestClock clock_;
EmptyStorageDelegate delegate_;
};
TEST_F(ConversionStorageSqlTest,
DatabaseInitialized_TablesAndIndexesInitialized) {
OpenDatabase();
CloseDatabase();
sql::Database raw_db;
EXPECT_TRUE(raw_db.Open(db_path()));
// [impressions] and [conversions].
EXPECT_EQ(2u, sql::test::CountSQLTables(&raw_db));
// [conversion_origin_idx], [impression_expiry_idx],
// [conversion_report_time_idx], [conversion_impression_id_idx].
EXPECT_EQ(4u, sql::test::CountSQLIndices(&raw_db));
}
TEST_F(ConversionStorageSqlTest, DatabaseReopened_DataPersisted) {
OpenDatabase();
AddReportToStorage();
EXPECT_EQ(1u, storage()->GetConversionsToReport(clock()->Now()).size());
CloseDatabase();
OpenDatabase();
EXPECT_EQ(1u, storage()->GetConversionsToReport(clock()->Now()).size());
}
TEST_F(ConversionStorageSqlTest, CorruptDatabase_RecoveredOnOpen) {
OpenDatabase();
AddReportToStorage();
EXPECT_EQ(1u, storage()->GetConversionsToReport(clock()->Now()).size());
CloseDatabase();
// Corrupt the database.
EXPECT_TRUE(sql::test::CorruptSizeInHeader(db_path()));
sql::test::ScopedErrorExpecter expecter;
expecter.ExpectError(SQLITE_CORRUPT);
// Open that database and ensure that it does not fail.
EXPECT_NO_FATAL_FAILURE(OpenDatabase());
// Data should be recovered.
EXPECT_EQ(1u, storage()->GetConversionsToReport(clock()->Now()).size());
EXPECT_TRUE(expecter.SawExpectedErrors());
}
} // namespace content
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/conversions/conversion_storage.h"
#include <list>
#include <memory>
#include <tuple>
#include <vector>
#include "base/files/scoped_temp_dir.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/simple_test_clock.h"
#include "content/browser/conversions/conversion_report.h"
#include "content/browser/conversions/conversion_storage_sql.h"
#include "content/browser/conversions/conversion_test_utils.h"
#include "content/browser/conversions/storable_conversion.h"
#include "content/browser/conversions/storable_impression.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace content {
namespace {
// Default max number of conversions for a single impression for testing.
const int kMaxConversions = 3;
// Default delay in milliseconds for when a report should be sent for testing.
const int kReportTime = 5;
using AttributionCredits = std::list<int>;
} // namespace
// Mock delegate which provides default behavior and delays reports by a fixed
// time from impression.
class MockStorageDelegate : public ConversionStorage::Delegate {
public:
MockStorageDelegate() = default;
virtual ~MockStorageDelegate() = default;
// ConversionStorage::Delegate
void ProcessNewConversionReports(
std::vector<ConversionReport>* reports) override {
for (auto& report : *reports) {
report.report_time = report.impression.impression_time() +
base::TimeDelta::FromMilliseconds(kReportTime);
// If attribution credits were provided, associate them with reports
// in order.
if (!attribution_credits_.empty()) {
report.attribution_credit = attribution_credits_.front();
attribution_credits_.pop_front();
}
}
}
int GetMaxConversionsPerImpression() const override {
return kMaxConversions;
}
void AddCredits(AttributionCredits credits) {
// Add all credits to our list in order.
attribution_credits_.splice(attribution_credits_.end(), credits);
}
private:
// List of attribution credits the mock delegate should associate with
// reports.
AttributionCredits attribution_credits_;
};
// Unit test suite for the ConversionStorage interface. All ConversionStorage
// implementations (including fakes) should be able to re-use this test suite.
class ConversionStorageTest : public testing::Test {
public:
ConversionStorageTest() {
EXPECT_TRUE(dir_.CreateUniqueTempDir());
storage_ = std::make_unique<ConversionStorageSql>(dir_.GetPath(),
&delegate_, &clock_);
EXPECT_TRUE(storage_->Initialize());
}
// Given a |conversion|, returns the expected conversion report properties at
// the current timestamp.
ConversionReport GetExpectedReport(const StorableImpression& impression,
const StorableConversion& conversion,
int attribution_credit = 0) {
ConversionReport report(
impression, conversion.conversion_data(),
clock_.Now() + base::TimeDelta::FromMilliseconds(kReportTime),
base::nullopt /* conversion_id */);
report.attribution_credit = attribution_credit;
return report;
}
void DeleteConversionReports(std::vector<ConversionReport> reports) {
for (auto report : reports) {
EXPECT_TRUE(storage_->DeleteConversion(*report.conversion_id));
}
}
void AddAttributionCredits(AttributionCredits credits) {
delegate_.AddCredits(credits);
}
base::SimpleTestClock* clock() { return &clock_; }
ConversionStorage* storage() { return storage_.get(); }
private:
MockStorageDelegate delegate_;
base::SimpleTestClock clock_;
base::ScopedTempDir dir_;
std::unique_ptr<ConversionStorage> storage_;
};
TEST_F(ConversionStorageTest,
GetWithNoMatchingImpressions_NoImpressionsReturned) {
EXPECT_EQ(
0, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
EXPECT_TRUE(storage()->GetConversionsToReport(clock()->Now()).empty());
}
TEST_F(ConversionStorageTest, GetWithMatchingImpression_ImpressionReturned) {
storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
EXPECT_EQ(
1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
}
TEST_F(ConversionStorageTest, MultipleImpressionsForConversion_AllConvert) {
storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
EXPECT_EQ(
2, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
}
TEST_F(ConversionStorageTest, ImpressionExpired_NoConversionsStored) {
storage()->StoreImpression(
ImpressionBuilder(clock()->Now())
.SetExpiry(base::TimeDelta::FromMilliseconds(2))
.Build());
clock()->Advance(base::TimeDelta::FromMilliseconds(2));
EXPECT_EQ(
0, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
}
TEST_F(ConversionStorageTest, ImpressionExpired_ConversionsStoredPrior) {
storage()->StoreImpression(
ImpressionBuilder(clock()->Now())
.SetExpiry(base::TimeDelta::FromMilliseconds(4))
.Build());
clock()->Advance(base::TimeDelta::FromMilliseconds(3));
EXPECT_EQ(
1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
clock()->Advance(base::TimeDelta::FromMilliseconds(5));
EXPECT_EQ(
0, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
}
TEST_F(ConversionStorageTest, ImpressionNotExpired_NotDeleted) {
storage()->StoreImpression(
ImpressionBuilder(clock()->Now())
.SetExpiry(base::TimeDelta::FromMilliseconds(3))
.Build());
EXPECT_EQ(0, storage()->DeleteExpiredImpressions());
}
TEST_F(ConversionStorageTest, ImpressionExpired_Deleted) {
storage()->StoreImpression(
ImpressionBuilder(clock()->Now())
.SetExpiry(base::TimeDelta::FromMilliseconds(3))
.Build());
clock()->Advance(base::TimeDelta::FromMilliseconds(3));
EXPECT_EQ(1, storage()->DeleteExpiredImpressions());
}
TEST_F(ConversionStorageTest,
ImpressionWithMaxConversions_ConversionReportNotStored) {
storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
for (int i = 0; i < kMaxConversions; i++) {
EXPECT_EQ(1, storage()->MaybeCreateAndStoreConversionReports(
DefaultConversion()));
}
// No additional conversion reports should be created.
EXPECT_EQ(
0, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
}
TEST_F(ConversionStorageTest, OneConversion_OneReportScheduled) {
auto impression = ImpressionBuilder(clock()->Now()).Build();
auto conversion = DefaultConversion();
storage()->StoreImpression(impression);
EXPECT_EQ(1, storage()->MaybeCreateAndStoreConversionReports(conversion));
ConversionReport expected_report = GetExpectedReport(impression, conversion);
clock()->Advance(base::TimeDelta::FromMilliseconds(kReportTime));
std::vector<ConversionReport> actual_reports =
storage()->GetConversionsToReport(clock()->Now());
EXPECT_TRUE(ReportsEqual({expected_report}, actual_reports));
}
TEST_F(ConversionStorageTest,
ConversionWithDifferentReportingOrigin_NoReportScheduled) {
auto impression = ImpressionBuilder(clock()->Now())
.SetReportingOrigin(
url::Origin::Create(GURL("https://different.test")))
.Build();
storage()->StoreImpression(impression);
EXPECT_EQ(
0, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
clock()->Advance(base::TimeDelta::FromMilliseconds(kReportTime));
EXPECT_EQ(0u, storage()->GetConversionsToReport(clock()->Now()).size());
}
TEST_F(ConversionStorageTest,
ConversionWithDifferentConversionOrigin_NoReportScheduled) {
auto impression = ImpressionBuilder(clock()->Now())
.SetConversionOrigin(
url::Origin::Create(GURL("https://different.test")))
.Build();
storage()->StoreImpression(impression);
EXPECT_EQ(
0, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
clock()->Advance(base::TimeDelta::FromMilliseconds(kReportTime));
EXPECT_EQ(0u, storage()->GetConversionsToReport(clock()->Now()).size());
}
TEST_F(ConversionStorageTest, OneConversion_AttributionCreditSet) {
auto impression = ImpressionBuilder(clock()->Now()).Build();
auto conversion = DefaultConversion();
const int kAttributionCredit = 100;
AddAttributionCredits({kAttributionCredit});
storage()->StoreImpression(impression);
EXPECT_EQ(1, storage()->MaybeCreateAndStoreConversionReports(conversion));
ConversionReport expected_report =
GetExpectedReport(impression, conversion, kAttributionCredit);
clock()->Advance(base::TimeDelta::FromMilliseconds(kReportTime));
std::vector<ConversionReport> actual_reports =
storage()->GetConversionsToReport(clock()->Now());
EXPECT_TRUE(ReportsEqual({expected_report}, actual_reports));
}
TEST_F(ConversionStorageTest,
ExpiredImpressionWithPendingConversion_NotDeleted) {
storage()->StoreImpression(
ImpressionBuilder(clock()->Now())
.SetExpiry(base::TimeDelta::FromMilliseconds(3))
.Build());
EXPECT_EQ(
1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
clock()->Advance(base::TimeDelta::FromMilliseconds(3));
EXPECT_EQ(0, storage()->DeleteExpiredImpressions());
}
TEST_F(ConversionStorageTest, TwoImpressionsOneExpired_OneDeleted) {
storage()->StoreImpression(
ImpressionBuilder(clock()->Now())
.SetExpiry(base::TimeDelta::FromMilliseconds(3))
.Build());
storage()->StoreImpression(
ImpressionBuilder(clock()->Now())
.SetExpiry(base::TimeDelta::FromMilliseconds(4))
.Build());
clock()->Advance(base::TimeDelta::FromMilliseconds(3));
EXPECT_EQ(
1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
EXPECT_EQ(1, storage()->DeleteExpiredImpressions());
}
TEST_F(ConversionStorageTest, ExpiredImpressionWithSentConversion_Deleted) {
storage()->StoreImpression(
ImpressionBuilder(clock()->Now())
.SetExpiry(base::TimeDelta::FromMilliseconds(3))
.Build());
EXPECT_EQ(
1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
clock()->Advance(base::TimeDelta::FromMilliseconds(3));
EXPECT_EQ(0, storage()->DeleteExpiredImpressions());
// Advance past the default report time.
clock()->Advance(base::TimeDelta::FromMilliseconds(kReportTime));
EXPECT_EQ(0, storage()->DeleteExpiredImpressions());
std::vector<ConversionReport> reports =
storage()->GetConversionsToReport(clock()->Now());
EXPECT_EQ(1u, reports.size());
DeleteConversionReports(reports);
EXPECT_EQ(1, storage()->DeleteExpiredImpressions());
}
TEST_F(ConversionStorageTest, ConversionReportDeleted_RemovedFromStorage) {
storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
EXPECT_EQ(
1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
clock()->Advance(base::TimeDelta::FromMilliseconds(kReportTime));
std::vector<ConversionReport> reports =
storage()->GetConversionsToReport(clock()->Now());
EXPECT_EQ(1u, reports.size());
DeleteConversionReports(reports);
EXPECT_TRUE(storage()->GetConversionsToReport(clock()->Now()).empty());
}
TEST_F(ConversionStorageTest,
ManyImpressionsWithManyConversions_ConversionReportsCreated) {
const int kNumMultiTouchImpressions = 20;
// Store a large, arbitrary number of impressions.
for (int i = 0; i < kNumMultiTouchImpressions; i++) {
storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
}
for (int i = 0; i < kMaxConversions; i++) {
EXPECT_EQ(
kNumMultiTouchImpressions,
storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
}
// No additional conversion reports should be created for any of the
// impressions.
EXPECT_EQ(
0, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
}
TEST_F(ConversionStorageTest,
NewImpressionForUnconvertedImpression_ImpressionRemainsActive) {
storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
auto new_impression =
ImpressionBuilder(clock()->Now())
.SetImpressionOrigin(url::Origin::Create(GURL("https://other.test/")))
.Build();
storage()->StoreImpression(new_impression);
// The first impression should be active because even though
// <reporting_origin, conversion_origin> matches, it has not converted yet.
EXPECT_EQ(
2, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
}
// This test makes sure that when a new click is received for a given
// <reporting_origin, conversion_origin> pair, all existing impressions for that
// origin that have converted are marked ineligible for new conversions per the
// multi-touch model.
TEST_F(ConversionStorageTest,
NewImpressionForConvertedImpression_MarkedInactive) {
storage()->StoreImpression(
ImpressionBuilder(clock()->Now()).SetData("0").Build());
EXPECT_EQ(
1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
clock()->Advance(base::TimeDelta::FromMilliseconds(kReportTime));
// Delete the report.
DeleteConversionReports(storage()->GetConversionsToReport(clock()->Now()));
// Store a new impression that should mark the first inactive.
auto new_impression =
ImpressionBuilder(clock()->Now()).SetData("1000").Build();
storage()->StoreImpression(new_impression);
// Only the new impression should convert.
auto conversion = DefaultConversion();
EXPECT_EQ(1, storage()->MaybeCreateAndStoreConversionReports(conversion));
ConversionReport expected_report =
GetExpectedReport(new_impression, conversion);
clock()->Advance(base::TimeDelta::FromMilliseconds(kReportTime));
// Verify it was the new impression that converted.
EXPECT_TRUE(ReportsEqual({expected_report},
storage()->GetConversionsToReport(clock()->Now())));
}
TEST_F(ConversionStorageTest,
NonMatchingImpressionForConvertedImpression_FirstRemainsActive) {
auto first_impression = ImpressionBuilder(clock()->Now()).Build();
storage()->StoreImpression(first_impression);
auto conversion = DefaultConversion();
EXPECT_EQ(
1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
// With the mock delegate, conversions are reported relative to impression
// time not conversion time. This report will match both the first and second
// conversion.
ConversionReport expected_report =
GetExpectedReport(first_impression, conversion);
clock()->Advance(base::TimeDelta::FromMilliseconds(kReportTime));
// Delete the report.
DeleteConversionReports(storage()->GetConversionsToReport(clock()->Now()));
// Store a new impression with a different reporting origin.
auto new_impression = ImpressionBuilder(clock()->Now())
.SetReportingOrigin(url::Origin::Create(
GURL("https://different.test")))
.Build();
storage()->StoreImpression(new_impression);
// The first impression should still be active and able to convert.
EXPECT_EQ(1, storage()->MaybeCreateAndStoreConversionReports(conversion));
// Verify it was the first impression that converted.
EXPECT_TRUE(ReportsEqual({expected_report},
storage()->GetConversionsToReport(clock()->Now())));
}
TEST_F(
ConversionStorageTest,
MultipleImpressionsForConversionAtDifferentTimes_AllImpressionsConverted) {
auto first_impression = ImpressionBuilder(clock()->Now()).Build();
storage()->StoreImpression(first_impression);
auto second_impression = ImpressionBuilder(clock()->Now()).Build();
storage()->StoreImpression(second_impression);
auto conversion = DefaultConversion();
ConversionReport first_expected_conversion =
GetExpectedReport(first_impression, conversion);
ConversionReport second_expected_conversion =
GetExpectedReport(second_impression, conversion);
// Advance clock so third impression is stored at a different timestamp.
clock()->Advance(base::TimeDelta::FromMilliseconds(3));
// Make a conversion with different impression data.
auto third_impression =
ImpressionBuilder(clock()->Now()).SetData("10").Build();
storage()->StoreImpression(third_impression);
ConversionReport third_expected_conversion =
GetExpectedReport(third_impression, conversion);
EXPECT_EQ(3, storage()->MaybeCreateAndStoreConversionReports(conversion));
clock()->Advance(base::TimeDelta::FromMilliseconds(kReportTime));
std::vector<ConversionReport> expected_reports = {first_expected_conversion,
second_expected_conversion,
third_expected_conversion};
std::vector<ConversionReport> actual_reports =
storage()->GetConversionsToReport(clock()->Now());
EXPECT_TRUE(ReportsEqual(expected_reports, actual_reports));
}
TEST_F(ConversionStorageTest,
ImpressionsAtDifferentTimes_ReportedAtDifferentTimes) {
auto first_impression = ImpressionBuilder(clock()->Now()).Build();
storage()->StoreImpression(first_impression);
// Advance clock so the next impression is stored at a different timestamp.
clock()->Advance(base::TimeDelta::FromMilliseconds(3));
storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
clock()->Advance(base::TimeDelta::FromMilliseconds(3));
storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
EXPECT_EQ(
3, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
// Advance to the first impression's report time and verify only its report is
// available.
clock()->Advance(base::TimeDelta::FromMilliseconds(kReportTime - 6));
EXPECT_EQ(1u, storage()->GetConversionsToReport(clock()->Now()).size());
clock()->Advance(base::TimeDelta::FromMilliseconds(3));
EXPECT_EQ(2u, storage()->GetConversionsToReport(clock()->Now()).size());
clock()->Advance(base::TimeDelta::FromMilliseconds(3));
EXPECT_EQ(3u, storage()->GetConversionsToReport(clock()->Now()).size());
}
TEST_F(ConversionStorageTest, GetConversionsToReportMultipleTimes_SameResult) {
storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
EXPECT_EQ(
1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
clock()->Advance(base::TimeDelta::FromMilliseconds(kReportTime));
std::vector<ConversionReport> first_call_reports =
storage()->GetConversionsToReport(clock()->Now());
std::vector<ConversionReport> second_call_reports =
storage()->GetConversionsToReport(clock()->Now());
// Expect that |GetConversionsToReport| did not delete any conversions.
EXPECT_EQ(1u, first_call_reports.size());
EXPECT_EQ(1u, second_call_reports.size());
EXPECT_TRUE(ReportsEqual(first_call_reports, second_call_reports));
}
TEST_F(ConversionStorageTest,
ManyImpressionsWithAttributionCredits_CreditsAssignedCorrectly) {
const int kNumImpressions = 10;
std::vector<ConversionReport> expected_reports;
AttributionCredits credits;
auto conversion = DefaultConversion();
// Store a large, arbitrary number of impressions.
for (int i = 0; i < kNumImpressions; i++) {
auto impression = ImpressionBuilder(clock()->Now())
.SetData(base::NumberToString(i))
.Build();
storage()->StoreImpression(impression);
expected_reports.push_back(GetExpectedReport(impression, conversion, i));
credits.push_back(i);
}
// Add the expected credits to the delegate.
AddAttributionCredits(credits);
EXPECT_EQ(kNumImpressions,
storage()->MaybeCreateAndStoreConversionReports(conversion));
// Verify that the attribution credits were associated with scheduled
// conversions as expected.
clock()->Advance(base::TimeDelta::FromMilliseconds(kReportTime));
std::vector<ConversionReport> actual_reports =
storage()->GetConversionsToReport(clock()->Now());
EXPECT_TRUE(ReportsEqual(expected_reports, actual_reports));
}
} // namespace content
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/conversions/conversion_test_utils.h"
#include <tuple>
#include "url/gurl.h"
namespace content {
namespace {
const char kDefaultImpressionOrigin[] = "https:/impression.test/";
const char kDefaultConversionOrigin[] = "https:/conversion.test/";
const char kDefaultReportOrigin[] = "https:/report.test/";
// Default expiry time for impressions for testing.
const int64_t kExpiryTime = 30;
} // namespace
int EmptyStorageDelegate::GetMaxConversionsPerImpression() const {
return 1;
}
// Builds an impression with default values. This is done as a builder because
// all values needed to be provided at construction time.
ImpressionBuilder::ImpressionBuilder(base::Time time)
: impression_data_("123"),
impression_time_(time),
expiry_(base::TimeDelta::FromMilliseconds(kExpiryTime)),
impression_origin_(url::Origin::Create(GURL(kDefaultImpressionOrigin))),
conversion_origin_(url::Origin::Create(GURL(kDefaultConversionOrigin))),
reporting_origin_(url::Origin::Create(GURL(kDefaultReportOrigin))) {}
ImpressionBuilder::~ImpressionBuilder() = default;
ImpressionBuilder& ImpressionBuilder::SetExpiry(base::TimeDelta delta) {
expiry_ = delta;
return *this;
}
ImpressionBuilder& ImpressionBuilder::SetData(const std::string& data) {
impression_data_ = data;
return *this;
}
ImpressionBuilder& ImpressionBuilder::SetImpressionOrigin(
const url::Origin& origin) {
impression_origin_ = origin;
return *this;
}
ImpressionBuilder& ImpressionBuilder::SetConversionOrigin(
const url::Origin& origin) {
conversion_origin_ = origin;
return *this;
}
ImpressionBuilder& ImpressionBuilder::SetReportingOrigin(
const url::Origin& origin) {
reporting_origin_ = origin;
return *this;
}
StorableImpression ImpressionBuilder::Build() const {
return StorableImpression(impression_data_, impression_origin_,
conversion_origin_, reporting_origin_,
impression_time_,
impression_time_ + expiry_ /* expiry_time */,
base::nullopt /* impression_id */);
}
StorableConversion DefaultConversion() {
StorableConversion conversion(
"111" /* conversion_data */,
url::Origin::Create(
GURL(kDefaultConversionOrigin)) /* conversion_origin */,
url::Origin::Create(GURL(kDefaultReportOrigin)) /* reporting_origin */);
return conversion;
}
// Custom comparator for comparing two vectors of conversion reports. Does not
// compare impression and conversion id's as they are set by the underlying
// sqlite db and should not be tested.
testing::AssertionResult ReportsEqual(
const std::vector<ConversionReport>& expected,
const std::vector<ConversionReport>& actual) {
const auto tie = [](const ConversionReport& conversion) {
return std::make_tuple(conversion.impression.impression_data(),
conversion.impression.impression_origin(),
conversion.impression.conversion_origin(),
conversion.impression.reporting_origin(),
conversion.impression.impression_time(),
conversion.impression.expiry_time(),
conversion.conversion_data, conversion.report_time,
conversion.attribution_credit);
};
if (expected.size() != actual.size())
return testing::AssertionFailure() << "Expected length " << expected.size()
<< ", actual: " << actual.size();
for (size_t i = 0; i < expected.size(); i++) {
if (tie(expected[i]) != tie(actual[i])) {
return testing::AssertionFailure()
<< "Expected " << expected[i] << " at index " << i
<< ", actual: " << actual[i];
}
}
return testing::AssertionSuccess();
}
} // namespace content
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CONTENT_BROWSER_CONVERSIONS_CONVERSION_TEST_UTILS_H_
#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_TEST_UTILS_H_
#include <string>
#include <vector>
#include "base/time/time.h"
#include "content/browser/conversions/conversion_report.h"
#include "content/browser/conversions/conversion_storage.h"
#include "content/browser/conversions/storable_conversion.h"
#include "content/browser/conversions/storable_impression.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "url/origin.h"
namespace content {
class EmptyStorageDelegate : public ConversionStorage::Delegate {
public:
EmptyStorageDelegate() = default;
virtual ~EmptyStorageDelegate() = default;
// ConversionStorage::Delegate
void ProcessNewConversionReports(
std::vector<ConversionReport>* reports) override {}
int GetMaxConversionsPerImpression() const override;
};
// Helper class to construct a StorableImpression for tests using default data.
// StorableImpression members are not mutable after construction requiring a
// builder pattern.
class ImpressionBuilder {
public:
ImpressionBuilder(base::Time time);
~ImpressionBuilder();
ImpressionBuilder& SetExpiry(base::TimeDelta delta);
ImpressionBuilder& SetData(const std::string& data);
ImpressionBuilder& SetImpressionOrigin(const url::Origin& origin);
ImpressionBuilder& SetConversionOrigin(const url::Origin& origin);
ImpressionBuilder& SetReportingOrigin(const url::Origin& origin);
StorableImpression Build() const;
private:
std::string impression_data_;
base::Time impression_time_;
base::TimeDelta expiry_;
url::Origin impression_origin_;
url::Origin conversion_origin_;
url::Origin reporting_origin_;
};
// Returns a StorableConversion with default data which matches the default
// impressions created by ImpressionBuilder.
StorableConversion DefaultConversion();
testing::AssertionResult ReportsEqual(
const std::vector<ConversionReport>& expected,
const std::vector<ConversionReport>& actual);
} // namespace content
#endif // CONTENT_BROWSER_CONVERSIONS_CONVERSION_TEST_UTILS_H_
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/conversions/storable_conversion.h"
#include "base/logging.h"
namespace content {
StorableConversion::StorableConversion(const std::string& conversion_data,
const url::Origin& conversion_origin,
const url::Origin& reporting_origin)
: conversion_data_(conversion_data),
conversion_origin_(conversion_origin),
reporting_origin_(reporting_origin) {
DCHECK(!reporting_origin_.opaque());
DCHECK(!conversion_origin_.opaque());
}
StorableConversion::StorableConversion(const StorableConversion& other) =
default;
StorableConversion::~StorableConversion() = default;
} // namespace content
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CONTENT_BROWSER_CONVERSIONS_STORABLE_CONVERSION_H_
#define CONTENT_BROWSER_CONVERSIONS_STORABLE_CONVERSION_H_
#include <string>
#include "base/time/time.h"
#include "content/common/content_export.h"
#include "url/origin.h"
namespace content {
// Struct which represents a conversion registration event that was observed in
// the renderer and is now being used by the browser process.
class CONTENT_EXPORT StorableConversion {
public:
// Should only be created with values that the browser process has already
// validated. At creation time, |conversion_data_| should already be stripped
// to a lower entropy. |conversion_origin| should be filled by a navigation
// origin known by the browser process.
StorableConversion(const std::string& conversion_data,
const url::Origin& conversion_origin,
const url::Origin& reporting_origin);
StorableConversion(const StorableConversion& other);
StorableConversion& operator=(const StorableConversion& other) = delete;
~StorableConversion();
const std::string& conversion_data() const { return conversion_data_; }
const url::Origin& conversion_origin() const { return conversion_origin_; }
const url::Origin& reporting_origin() const { return reporting_origin_; }
private:
// Conversion data associated with conversion registration event. String
// representing a valid hexadecimal number.
std::string conversion_data_;
// Origin this conversion event occurred on.
url::Origin conversion_origin_;
// Origin of the conversion redirect url, and the origin that will receive any
// reports.
url::Origin reporting_origin_;
};
} // namespace content
#endif // CONTENT_BROWSER_CONVERSIONS_STORABLE_CONVERSION_H_
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/conversions/storable_impression.h"
#include "base/logging.h"
namespace content {
StorableImpression::StorableImpression(
const std::string& impression_data,
const url::Origin& impression_origin,
const url::Origin& conversion_origin,
const url::Origin& reporting_origin,
base::Time impression_time,
base::Time expiry_time,
const base::Optional<int64_t>& impression_id)
: impression_data_(impression_data),
impression_origin_(impression_origin),
conversion_origin_(conversion_origin),
reporting_origin_(reporting_origin),
impression_time_(impression_time),
expiry_time_(expiry_time),
impression_id_(impression_id) {
DCHECK(!impression_origin.opaque());
DCHECK(!reporting_origin.opaque());
DCHECK(!conversion_origin.opaque());
}
StorableImpression::StorableImpression(const StorableImpression& other) =
default;
StorableImpression::~StorableImpression() = default;
} // namespace content
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CONTENT_BROWSER_CONVERSIONS_STORABLE_IMPRESSION_H_
#define CONTENT_BROWSER_CONVERSIONS_STORABLE_IMPRESSION_H_
#include <stdint.h>
#include <string>
#include "base/optional.h"
#include "base/time/time.h"
#include "content/common/content_export.h"
#include "url/origin.h"
namespace content {
// Struct which represents all stored attributes of an impression. All values
// should be sanitized before creating this object.
class CONTENT_EXPORT StorableImpression {
public:
// If |impression_id| is not available, 0 should be provided.
StorableImpression(const std::string& impression_data,
const url::Origin& impression_origin,
const url::Origin& conversion_origin,
const url::Origin& reporting_origin,
base::Time impression_time,
base::Time expiry_time,
const base::Optional<int64_t>& impression_id);
StorableImpression(const StorableImpression& other);
StorableImpression& operator=(const StorableImpression& other) = delete;
~StorableImpression();
const std::string& impression_data() const { return impression_data_; }
const url::Origin& impression_origin() const { return impression_origin_; }
const url::Origin& conversion_origin() const { return conversion_origin_; }
const url::Origin& reporting_origin() const { return reporting_origin_; }
base::Time impression_time() const { return impression_time_; }
base::Time expiry_time() const { return expiry_time_; }
base::Optional<int64_t> impression_id() const { return impression_id_; }
private:
// String representing a valid hexadecimal number.
std::string impression_data_;
url::Origin impression_origin_;
url::Origin conversion_origin_;
url::Origin reporting_origin_;
base::Time impression_time_;
base::Time expiry_time_;
// If null, an ID has not been assigned yet.
base::Optional<int64_t> impression_id_;
};
} // namespace content
#endif // CONTENT_BROWSER_CONVERSIONS_STORABLE_IMPRESSION_H_
...@@ -50,6 +50,8 @@ jumbo_static_library("test_support") { ...@@ -50,6 +50,8 @@ jumbo_static_library("test_support") {
"../browser/background_fetch/mock_background_fetch_delegate.h", "../browser/background_fetch/mock_background_fetch_delegate.h",
"../browser/browsing_data/browsing_data_test_utils.cc", "../browser/browsing_data/browsing_data_test_utils.cc",
"../browser/browsing_data/browsing_data_test_utils.h", "../browser/browsing_data/browsing_data_test_utils.h",
"../browser/conversions/conversion_test_utils.cc",
"../browser/conversions/conversion_test_utils.h",
"../browser/media/session/mock_media_session_player_observer.cc", "../browser/media/session/mock_media_session_player_observer.cc",
"../browser/media/session/mock_media_session_player_observer.h", "../browser/media/session/mock_media_session_player_observer.h",
"../browser/media/session/mock_media_session_service_impl.cc", "../browser/media/session/mock_media_session_service_impl.cc",
...@@ -1554,6 +1556,8 @@ test("content_unittests") { ...@@ -1554,6 +1556,8 @@ test("content_unittests") {
"../browser/code_cache/generated_code_cache_unittest.cc", "../browser/code_cache/generated_code_cache_unittest.cc",
"../browser/content_index/content_index_database_unittest.cc", "../browser/content_index/content_index_database_unittest.cc",
"../browser/content_index/content_index_service_impl_unittest.cc", "../browser/content_index/content_index_service_impl_unittest.cc",
"../browser/conversions/conversion_storage_sql_unittest.cc",
"../browser/conversions/conversion_storage_unittest.cc",
"../browser/cookie_store/cookie_store_manager_unittest.cc", "../browser/cookie_store/cookie_store_manager_unittest.cc",
"../browser/devtools/devtools_background_services_context_impl_unittest.cc", "../browser/devtools/devtools_background_services_context_impl_unittest.cc",
"../browser/devtools/devtools_http_handler_unittest.cc", "../browser/devtools/devtools_http_handler_unittest.cc",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment