summaryrefslogtreecommitdiff
path: root/chromium/content/browser/conversions
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/content/browser/conversions')
-rw-r--r--chromium/content/browser/conversions/BUILD.gn10
-rw-r--r--chromium/content/browser/conversions/OWNERS3
-rw-r--r--chromium/content/browser/conversions/conversion_host.cc190
-rw-r--r--chromium/content/browser/conversions/conversion_host.h51
-rw-r--r--chromium/content/browser/conversions/conversion_host_unittest.cc246
-rw-r--r--chromium/content/browser/conversions/conversion_internals.mojom54
-rw-r--r--chromium/content/browser/conversions/conversion_internals_browsertest.cc284
-rw-r--r--chromium/content/browser/conversions/conversion_internals_handler_impl.cc129
-rw-r--r--chromium/content/browser/conversions/conversion_internals_handler_impl.h56
-rw-r--r--chromium/content/browser/conversions/conversion_internals_ui.cc59
-rw-r--r--chromium/content/browser/conversions/conversion_internals_ui.h47
-rw-r--r--chromium/content/browser/conversions/conversion_manager.cc75
-rw-r--r--chromium/content/browser/conversions/conversion_manager.h110
-rw-r--r--chromium/content/browser/conversions/conversion_manager_impl.cc282
-rw-r--r--chromium/content/browser/conversions/conversion_manager_impl.h182
-rw-r--r--chromium/content/browser/conversions/conversion_manager_impl_unittest.cc365
-rw-r--r--chromium/content/browser/conversions/conversion_network_sender_impl.cc177
-rw-r--r--chromium/content/browser/conversions/conversion_network_sender_impl.h71
-rw-r--r--chromium/content/browser/conversions/conversion_network_sender_impl_unittest.cc252
-rw-r--r--chromium/content/browser/conversions/conversion_page_metrics.cc24
-rw-r--r--chromium/content/browser/conversions/conversion_page_metrics.h33
-rw-r--r--chromium/content/browser/conversions/conversion_policy.cc122
-rw-r--r--chromium/content/browser/conversions/conversion_policy.h44
-rw-r--r--chromium/content/browser/conversions/conversion_policy_unittest.cc127
-rw-r--r--chromium/content/browser/conversions/conversion_registration_browsertest.cc20
-rw-r--r--chromium/content/browser/conversions/conversion_report.cc1
-rw-r--r--chromium/content/browser/conversions/conversion_report.h4
-rw-r--r--chromium/content/browser/conversions/conversion_reporter_impl.cc108
-rw-r--r--chromium/content/browser/conversions/conversion_reporter_impl.h111
-rw-r--r--chromium/content/browser/conversions/conversion_reporter_impl_unittest.cc203
-rw-r--r--chromium/content/browser/conversions/conversion_storage.h39
-rw-r--r--chromium/content/browser/conversions/conversion_storage_delegate_impl.cc110
-rw-r--r--chromium/content/browser/conversions/conversion_storage_delegate_impl.h53
-rw-r--r--chromium/content/browser/conversions/conversion_storage_delegate_impl_unittest.cc148
-rw-r--r--chromium/content/browser/conversions/conversion_storage_sql.cc310
-rw-r--r--chromium/content/browser/conversions/conversion_storage_sql.h24
-rw-r--r--chromium/content/browser/conversions/conversion_storage_sql_unittest.cc169
-rw-r--r--chromium/content/browser/conversions/conversion_storage_unittest.cc357
-rw-r--r--chromium/content/browser/conversions/conversion_test_utils.cc142
-rw-r--r--chromium/content/browser/conversions/conversion_test_utils.h113
-rw-r--r--chromium/content/browser/conversions/conversions_browsertest.cc254
-rw-r--r--chromium/content/browser/conversions/impression_declaration_browsertest.cc478
-rw-r--r--chromium/content/browser/conversions/storable_conversion.cc2
-rw-r--r--chromium/content/browser/conversions/storable_impression.cc2
-rw-r--r--chromium/content/browser/conversions/storable_impression.h3
45 files changed, 5201 insertions, 443 deletions
diff --git a/chromium/content/browser/conversions/BUILD.gn b/chromium/content/browser/conversions/BUILD.gn
new file mode 100644
index 00000000000..6d722613e99
--- /dev/null
+++ b/chromium/content/browser/conversions/BUILD.gn
@@ -0,0 +1,10 @@
+# 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.
+
+import("//mojo/public/tools/bindings/mojom.gni")
+
+mojom("mojo_bindings") {
+ sources = [ "conversion_internals.mojom" ]
+ public_deps = [ "//url/mojom:url_mojom_origin" ]
+}
diff --git a/chromium/content/browser/conversions/OWNERS b/chromium/content/browser/conversions/OWNERS
index 7f1bf1a18d0..b716879726c 100644
--- a/chromium/content/browser/conversions/OWNERS
+++ b/chromium/content/browser/conversions/OWNERS
@@ -1,5 +1,8 @@
csharrison@chromium.org
johnidel@chromium.org
+per-file *.mojom=set noparent
+per-file *.mojom=file://ipc/SECURITY_OWNERS
+
# TEAM: privacy-sandbox-dev@chromium.org
# COMPONENT: Internals>ConversionMeasurement
diff --git a/chromium/content/browser/conversions/conversion_host.cc b/chromium/content/browser/conversions/conversion_host.cc
index ec917fabbb2..0a695271ca4 100644
--- a/chromium/content/browser/conversions/conversion_host.cc
+++ b/chromium/content/browser/conversions/conversion_host.cc
@@ -6,32 +6,180 @@
#include "base/bind.h"
#include "base/bind_helpers.h"
+#include "base/check.h"
+#include "base/memory/ptr_util.h"
#include "content/browser/conversions/conversion_manager.h"
+#include "content/browser/conversions/conversion_manager_impl.h"
+#include "content/browser/conversions/conversion_page_metrics.h"
#include "content/browser/conversions/conversion_policy.h"
-#include "content/browser/conversions/storable_conversion.h"
+#include "content/browser/frame_host/frame_tree.h"
+#include "content/browser/frame_host/frame_tree_node.h"
+#include "content/browser/frame_host/render_frame_host_impl.h"
#include "content/browser/storage_partition_impl.h"
#include "content/public/browser/browser_context.h"
+#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
-#include "content/public/browser/web_contents.h"
#include "mojo/public/cpp/bindings/message.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
+#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
#include "url/origin.h"
namespace content {
-ConversionHost::ConversionHost(WebContents* contents)
- : web_contents_(contents), receiver_(contents, this) {}
+namespace {
-ConversionHost::~ConversionHost() = default;
+// Abstraction that wraps an iterator to a map. When this goes out of the scope,
+// the underlying iterator is erased from the map. This is useful for control
+// flows where map cleanup needs to occur regardless of additional early exit
+// logic.
+template <typename Map>
+class ScopedMapDeleter {
+ public:
+ ScopedMapDeleter(Map* map, const typename Map::key_type& key)
+ : map_(map), it_(map_->find(key)) {}
+ ~ScopedMapDeleter() {
+ if (*this)
+ map_->erase(it_);
+ }
+
+ typename Map::iterator* get() { return &it_; }
+
+ explicit operator bool() const { return it_ != map_->end(); }
+
+ private:
+ Map* map_;
+ typename Map::iterator it_;
+};
+
+} // namespace
+
+// static
+std::unique_ptr<ConversionHost> ConversionHost::CreateForTesting(
+ WebContents* web_contents,
+ std::unique_ptr<ConversionManager::Provider> conversion_manager_provider) {
+ return base::WrapUnique(
+ new ConversionHost(web_contents, std::move(conversion_manager_provider)));
+}
+
+ConversionHost::ConversionHost(WebContents* web_contents)
+ : ConversionHost(web_contents,
+ std::make_unique<ConversionManagerProviderImpl>()) {}
+
+ConversionHost::ConversionHost(
+ WebContents* web_contents,
+ std::unique_ptr<ConversionManager::Provider> conversion_manager_provider)
+ : WebContentsObserver(web_contents),
+ conversion_manager_provider_(std::move(conversion_manager_provider)),
+ receiver_(web_contents, this) {
+ // TODO(csharrison): When https://crbug.com/1051334 is resolved, add a DCHECK
+ // that the kConversionMeasurement feature is enabled.
+}
+
+ConversionHost::~ConversionHost() {
+ DCHECK_EQ(0u, navigation_impression_origins_.size());
+}
+
+void ConversionHost::DidStartNavigation(NavigationHandle* navigation_handle) {
+ // Navigations with an impression set should only occur in the main frame.
+ if (!navigation_handle->GetImpression() ||
+ !navigation_handle->IsInMainFrame() ||
+ !conversion_manager_provider_->GetManager(web_contents())) {
+ return;
+ }
+
+ RenderFrameHostImpl* initiator_frame_host =
+ RenderFrameHostImpl::FromID(navigation_handle->GetInitiatorRoutingId());
+
+ // The initiator frame host may be deleted by this point. In that case, ignore
+ // this navigation and drop the impression associated with it.
+ // TODO(https://crbug.com/1056907): Record metrics on how often impressions
+ // are dropped because the initiator is destroyed.
+ if (!initiator_frame_host)
+ return;
+
+ // Look up the initiator root's origin which will be used as the impression
+ // origin. This works because we won't update the origin for the initiator RFH
+ // until we receive confirmation from the renderer that it has committed.
+ // Since frame mutation is all serialized on the Blink main thread, we get an
+ // implicit ordering: a navigation with an impression attached won't be
+ // processed after a navigation commit in the initiator RFH, so reading the
+ // origin off is safe at the start of the navigation.
+ const url::Origin& initiator_root_frame_origin =
+ initiator_frame_host->frame_tree_node()
+ ->frame_tree()
+ ->root()
+ ->current_origin();
+ navigation_impression_origins_.emplace(navigation_handle->GetNavigationId(),
+ initiator_root_frame_origin);
+}
+
+void ConversionHost::DidFinishNavigation(NavigationHandle* navigation_handle) {
+ ConversionManager* conversion_manager =
+ conversion_manager_provider_->GetManager(web_contents());
+ if (!conversion_manager) {
+ DCHECK(navigation_impression_origins_.empty());
+ return;
+ }
+
+ ScopedMapDeleter<NavigationImpressionOriginMap> it(
+ &navigation_impression_origins_, navigation_handle->GetNavigationId());
+
+ // If an impression is not associated with a main frame navigation, ignore it.
+ // If the navigation did not commit, committed to a Chrome error page, or was
+ // same document, ignore it. Impressions should never be attached to
+ // same-document navigations but can be the result of a bad renderer.
+ if (!navigation_handle->IsInMainFrame() ||
+ !navigation_handle->HasCommitted() || navigation_handle->IsErrorPage() ||
+ navigation_handle->IsSameDocument()) {
+ return;
+ }
+
+ conversion_page_metrics_ = std::make_unique<ConversionPageMetrics>();
+
+ // If we were not able to access the impression origin, ignore the navigation.
+ if (!it)
+ return;
+ url::Origin impression_origin = std::move((*it.get())->second);
+ DCHECK(navigation_handle->GetImpression());
+ const Impression& impression = *(navigation_handle->GetImpression());
+
+ // If the impression's conversion destination does not match the final top
+ // frame origin of this new navigation ignore it.
+ if (impression.conversion_destination !=
+ navigation_handle->GetRenderFrameHost()->GetLastCommittedOrigin()) {
+ return;
+ }
+
+ // Convert |impression| into a StorableImpression that can be forwarded to
+ // storage. If a reporting origin was not provided, default to the conversion
+ // destination for reporting.
+ const url::Origin& reporting_origin = !impression.reporting_origin
+ ? impression_origin
+ : *impression.reporting_origin;
+
+ // Conversion measurement is only allowed in secure contexts.
+ if (!network::IsOriginPotentiallyTrustworthy(impression_origin) ||
+ !network::IsOriginPotentiallyTrustworthy(reporting_origin) ||
+ !network::IsOriginPotentiallyTrustworthy(
+ impression.conversion_destination)) {
+ // TODO (1049654): This should log a console error when it occurs.
+ return;
+ }
+
+ base::Time impression_time = base::Time::Now();
+ const ConversionPolicy& policy = conversion_manager->GetConversionPolicy();
+ StorableImpression storable_impression(
+ policy.GetSanitizedImpressionData(impression.impression_data),
+ impression_origin, impression.conversion_destination, reporting_origin,
+ impression_time,
+ policy.GetExpiryTimeForImpression(impression.expiry, impression_time),
+ /*impression_id=*/base::nullopt);
+
+ conversion_manager->HandleImpression(storable_impression);
+}
-// TODO(https://crbug.com/1044099): Limit the number of conversion redirects per
-// page-load to a reasonable number.
void ConversionHost::RegisterConversion(
blink::mojom::ConversionPtr conversion) {
- // If there is no conversion manager available, ignore any conversion
- // registrations.
- if (!GetManager())
- return;
content::RenderFrameHost* render_frame_host =
receiver_.GetCurrentTargetFrame();
@@ -42,6 +190,13 @@ void ConversionHost::RegisterConversion(
return;
}
+ // If there is no conversion manager available, ignore any conversion
+ // registrations.
+ ConversionManager* conversion_manager =
+ conversion_manager_provider_->GetManager(web_contents());
+ if (!conversion_manager)
+ return;
+
// Only allow conversion registration on secure pages with a secure conversion
// redirects.
if (!network::IsOriginPotentiallyTrustworthy(
@@ -54,12 +209,14 @@ void ConversionHost::RegisterConversion(
}
StorableConversion storable_conversion(
- GetManager()->GetConversionPolicy().GetSanitizedConversionData(
+ conversion_manager->GetConversionPolicy().GetSanitizedConversionData(
conversion->conversion_data),
render_frame_host->GetLastCommittedOrigin(),
conversion->reporting_origin);
- GetManager()->HandleConversion(storable_conversion);
+ if (conversion_page_metrics_)
+ conversion_page_metrics_->OnConversion(storable_conversion);
+ conversion_manager->HandleConversion(storable_conversion);
}
void ConversionHost::SetCurrentTargetFrameForTesting(
@@ -67,11 +224,4 @@ void ConversionHost::SetCurrentTargetFrameForTesting(
receiver_.SetCurrentTargetFrameForTesting(render_frame_host);
}
-ConversionManager* ConversionHost::GetManager() {
- return static_cast<StoragePartitionImpl*>(
- BrowserContext::GetDefaultStoragePartition(
- web_contents_->GetBrowserContext()))
- ->GetConversionManager();
-}
-
} // namespace content
diff --git a/chromium/content/browser/conversions/conversion_host.h b/chromium/content/browser/conversions/conversion_host.h
index 468fa6b5b75..169c08a8042 100644
--- a/chromium/content/browser/conversions/conversion_host.h
+++ b/chromium/content/browser/conversions/conversion_host.h
@@ -5,21 +5,32 @@
#ifndef CONTENT_BROWSER_CONVERSIONS_CONVERSION_HOST_H_
#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_HOST_H_
+#include <memory>
+
+#include "base/containers/flat_map.h"
#include "base/gtest_prod_util.h"
+#include "content/browser/conversions/conversion_manager.h"
+#include "content/common/content_export.h"
+#include "content/public/browser/web_contents_observer.h"
#include "content/public/browser/web_contents_receiver_set.h"
#include "third_party/blink/public/mojom/conversions/conversions.mojom.h"
namespace content {
-class ConversionManager;
+class ConversionPageMetrics;
class RenderFrameHost;
class WebContents;
// Class responsible for listening to conversion events originating from blink,
// and verifying that they are valid. Owned by the WebContents. Lifetime is
// bound to lifetime of the WebContents.
-class CONTENT_EXPORT ConversionHost : public blink::mojom::ConversionHost {
+class CONTENT_EXPORT ConversionHost : public WebContentsObserver,
+ public blink::mojom::ConversionHost {
public:
+ static std::unique_ptr<ConversionHost> CreateForTesting(
+ WebContents* web_contents,
+ std::unique_ptr<ConversionManager::Provider> conversion_manager_provider);
+
explicit ConversionHost(WebContents* web_contents);
ConversionHost(const ConversionHost& other) = delete;
ConversionHost& operator=(const ConversionHost& other) = delete;
@@ -32,17 +43,47 @@ class CONTENT_EXPORT ConversionHost : public blink::mojom::ConversionHost {
FRIEND_TEST_ALL_PREFIXES(ConversionHostTest,
ConversionWithInsecureReportingOrigin_BadMessage);
FRIEND_TEST_ALL_PREFIXES(ConversionHostTest, ValidConversion_NoBadMessage);
+ FRIEND_TEST_ALL_PREFIXES(ConversionHostTest, PerPageConversionMetrics);
+ FRIEND_TEST_ALL_PREFIXES(ConversionHostTest,
+ NoManager_NoPerPageConversionMetrics);
+
+ ConversionHost(
+ WebContents* web_contents,
+ std::unique_ptr<ConversionManager::Provider> conversion_manager_provider);
// blink::mojom::ConversionHost:
void RegisterConversion(blink::mojom::ConversionPtr conversion) override;
+ // WebContentsObserver:
+ void DidStartNavigation(NavigationHandle* navigation_handle) override;
+ void DidFinishNavigation(NavigationHandle* navigation_handle) override;
+
// Sets the target frame on |receiver_|.
void SetCurrentTargetFrameForTesting(RenderFrameHost* render_frame_host);
- // Gets the manager for this web contents. Can be null.
- ConversionManager* GetManager();
+ // Map which stores the top-frame origin an impression occurred on for all
+ // navigations with an associated impression, keyed by navigation ID.
+ // Initiator origins are stored at navigation start time to have the best
+ // chance of catching the initiating frame before it has a chance to go away.
+ // Storing the origins at navigation start also prevents cases where a frame
+ // initiates a navigation for itself, causing the frame to be correct but not
+ // representing the frame state at the time the navigation was initiated. They
+ // are stored until DidFinishNavigation, when they can be matched up with an
+ // impression.
+ //
+ // A flat_map is used as the number of ongoing impression navigations is
+ // expected to be very small in a given WebContents.
+ using NavigationImpressionOriginMap = base::flat_map<int64_t, url::Origin>;
+ NavigationImpressionOriginMap navigation_impression_origins_;
+
+ // Gives access to a ConversionManager implementation to forward impressions
+ // and conversion registrations to.
+ std::unique_ptr<ConversionManager::Provider> conversion_manager_provider_;
- WebContents* web_contents_;
+ // Logs metrics per top-level page load. Created for every top level
+ // navigation that commits, as long as there is a ConversionManager.
+ // Excludes the initial about:blank document.
+ std::unique_ptr<ConversionPageMetrics> conversion_page_metrics_;
WebContentsFrameReceiverSet<blink::mojom::ConversionHost> receiver_;
};
diff --git a/chromium/content/browser/conversions/conversion_host_unittest.cc b/chromium/content/browser/conversions/conversion_host_unittest.cc
index 255587e9365..2769ef893d8 100644
--- a/chromium/content/browser/conversions/conversion_host_unittest.cc
+++ b/chromium/content/browser/conversions/conversion_host_unittest.cc
@@ -6,31 +6,51 @@
#include <memory>
+#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
+#include "content/browser/conversions/conversion_manager.h"
+#include "content/browser/conversions/conversion_test_utils.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/common/content_features.h"
#include "content/public/test/test_renderer_host.h"
#include "content/test/fake_mojo_message_dispatch_context.h"
+#include "content/test/navigation_simulator_impl.h"
#include "content/test/test_render_frame_host.h"
#include "content/test/test_web_contents.h"
#include "mojo/public/cpp/test_support/test_utils.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/conversions/conversions.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace content {
+namespace {
+
+const char kConversionUrl[] = "https://b.com";
+
+Impression CreateValidImpression() {
+ Impression result;
+ result.conversion_destination = url::Origin::Create(GURL(kConversionUrl));
+ result.reporting_origin = url::Origin::Create(GURL("https://c.com"));
+ result.impression_data = 1UL;
+ return result;
+}
+
+} // namespace
+
class ConversionHostTest : public RenderViewHostTestHarness {
public:
- ConversionHostTest() {
- feature_list_.InitAndEnableFeature(features::kConversionMeasurement);
- }
+ ConversionHostTest() = default;
void SetUp() override {
RenderViewHostTestHarness::SetUp();
static_cast<WebContentsImpl*>(web_contents())
->RemoveReceiverSetForTesting(blink::mojom::ConversionHost::Name_);
- conversion_host_ = std::make_unique<ConversionHost>(web_contents());
+
+ conversion_host_ = ConversionHost::CreateForTesting(
+ web_contents(), std::make_unique<TestManagerProvider>(&test_manager_));
contents()->GetMainFrame()->InitializeRenderFrameIfNeeded();
}
@@ -40,8 +60,10 @@ class ConversionHostTest : public RenderViewHostTestHarness {
ConversionHost* conversion_host() { return conversion_host_.get(); }
+ protected:
+ TestConversionManager test_manager_;
+
private:
- base::test::ScopedFeatureList feature_list_;
std::unique_ptr<ConversionHost> conversion_host_;
};
@@ -64,6 +86,7 @@ TEST_F(ConversionHostTest, ConversionInSubframe_BadMessage) {
conversion_host()->RegisterConversion(std::move(conversion));
EXPECT_EQ("blink.mojom.ConversionHost can only be used by the main frame.",
bad_message_observer.WaitForBadMessage());
+ EXPECT_EQ(0u, test_manager_.num_conversions());
}
TEST_F(ConversionHostTest, ConversionOnInsecurePage_BadMessage) {
@@ -83,6 +106,7 @@ TEST_F(ConversionHostTest, ConversionOnInsecurePage_BadMessage) {
"blink.mojom.ConversionHost can only be used in secure contexts with a "
"secure conversion registration origin.",
bad_message_observer.WaitForBadMessage());
+ EXPECT_EQ(0u, test_manager_.num_conversions());
}
TEST_F(ConversionHostTest, ConversionWithInsecureReportingOrigin_BadMessage) {
@@ -101,14 +125,15 @@ TEST_F(ConversionHostTest, ConversionWithInsecureReportingOrigin_BadMessage) {
"blink.mojom.ConversionHost can only be used in secure contexts with a "
"secure conversion registration origin.",
bad_message_observer.WaitForBadMessage());
+ EXPECT_EQ(0u, test_manager_.num_conversions());
}
TEST_F(ConversionHostTest, ValidConversion_NoBadMessage) {
- // Create a page with an insecure origin.
+ // Create a page with a secure origin.
contents()->NavigateAndCommit(GURL("https://www.example.com"));
conversion_host()->SetCurrentTargetFrameForTesting(main_rfh());
- // Create a fake dispatch context to trigger a bad message in.
+ // Create a fake dispatch context to listen for bad messages.
FakeMojoMessageDispatchContext fake_dispatch_context;
mojo::test::BadMessageObserver bad_message_observer;
@@ -121,6 +146,213 @@ TEST_F(ConversionHostTest, ValidConversion_NoBadMessage) {
// triggered.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(bad_message_observer.got_bad_message());
+ EXPECT_EQ(1u, test_manager_.num_conversions());
+}
+
+TEST_F(ConversionHostTest, PerPageConversionMetrics) {
+ base::HistogramTester histograms;
+
+ contents()->NavigateAndCommit(GURL("https://www.example.com"));
+
+ // Initial document should not log metrics.
+ histograms.ExpectTotalCount("Conversions.RegisteredConversionsPerPage", 0);
+
+ conversion_host()->SetCurrentTargetFrameForTesting(main_rfh());
+ blink::mojom::ConversionPtr conversion = blink::mojom::Conversion::New();
+ conversion->reporting_origin =
+ url::Origin::Create(GURL("https://secure.com"));
+
+ for (size_t i = 0u; i < 8u; i++) {
+ conversion_host()->RegisterConversion(conversion->Clone());
+ EXPECT_EQ(1u, test_manager_.num_conversions());
+ test_manager_.Reset();
+ }
+
+ // Same document navs should not reset the counter.
+ contents()->NavigateAndCommit(GURL("https://www.example.com#hash"));
+ histograms.ExpectTotalCount("Conversions.RegisteredConversionsPerPage", 0);
+
+ // Re-navigating should reset the counter.
+ contents()->NavigateAndCommit(GURL("https://www.example-next.com"));
+ histograms.ExpectUniqueSample("Conversions.RegisteredConversionsPerPage", 8,
+ 1);
+}
+
+TEST_F(ConversionHostTest, NoManager_NoPerPageConversionMetrics) {
+ // Replace the ConversionHost on the WebContents with one that is backed by a
+ // null ConversionManager.
+ static_cast<WebContentsImpl*>(web_contents())
+ ->RemoveReceiverSetForTesting(blink::mojom::ConversionHost::Name_);
+ auto conversion_host = ConversionHost::CreateForTesting(
+ web_contents(), std::make_unique<TestManagerProvider>(nullptr));
+ contents()->NavigateAndCommit(GURL("https://www.example.com"));
+
+ base::HistogramTester histograms;
+ conversion_host->SetCurrentTargetFrameForTesting(main_rfh());
+ blink::mojom::ConversionPtr conversion = blink::mojom::Conversion::New();
+ conversion->reporting_origin =
+ url::Origin::Create(GURL("https://secure.com"));
+ conversion_host->RegisterConversion(std::move(conversion));
+
+ // Navigate again to trigger histogram code.
+ contents()->NavigateAndCommit(GURL("https://www.example-next.com"));
+ histograms.ExpectBucketCount("Conversions.RegisteredConversionsPerPage", 1,
+ 0);
+}
+
+TEST_F(ConversionHostTest, NavigationWithNoImpression_Ignored) {
+ contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
+ NavigationSimulatorImpl::NavigateAndCommitFromDocument(GURL(kConversionUrl),
+ main_rfh());
+
+ EXPECT_EQ(0u, test_manager_.num_impressions());
+}
+
+TEST_F(ConversionHostTest, ValidImpression_ForwardedToManager) {
+ contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
+ auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
+ GURL(kConversionUrl), main_rfh());
+ navigation->SetInitiatorFrame(main_rfh());
+ navigation->set_impression(CreateValidImpression());
+ navigation->Commit();
+
+ EXPECT_EQ(1u, test_manager_.num_impressions());
+}
+
+TEST_F(ConversionHostTest, ImpressionWithNoManagerAvilable_NoCrash) {
+ // Replace the ConversionHost on the WebContents with one that is backed by a
+ // null ConversionManager.
+ static_cast<WebContentsImpl*>(web_contents())
+ ->RemoveReceiverSetForTesting(blink::mojom::ConversionHost::Name_);
+ auto conversion_host = ConversionHost::CreateForTesting(
+ web_contents(), std::make_unique<TestManagerProvider>(nullptr));
+
+ auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
+ GURL(kConversionUrl), main_rfh());
+ navigation->SetInitiatorFrame(main_rfh());
+ navigation->set_impression(CreateValidImpression());
+ navigation->Commit();
+}
+
+TEST_F(ConversionHostTest, ImpressionInSubframe_Ignored) {
+ contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
+
+ // Create a subframe and use it as a target for the conversion registration
+ // mojo.
+ content::RenderFrameHostTester* rfh_tester =
+ content::RenderFrameHostTester::For(main_rfh());
+ content::RenderFrameHost* subframe = rfh_tester->AppendChild("subframe");
+
+ auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
+ GURL(kConversionUrl), subframe);
+ navigation->SetInitiatorFrame(main_rfh());
+ navigation->set_impression(CreateValidImpression());
+ navigation->Commit();
+
+ EXPECT_EQ(0u, test_manager_.num_impressions());
+}
+
+// Test that if we cannot access the initiator frame of the navigation, we
+// ignore the associated impression.
+TEST_F(ConversionHostTest, ImpressionNavigationWithDeadInitiator_Ignored) {
+ contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
+
+ auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
+ GURL(kConversionUrl), main_rfh());
+ navigation->set_impression(CreateValidImpression());
+ navigation->Commit();
+
+ EXPECT_EQ(0u, test_manager_.num_impressions());
+}
+
+TEST_F(ConversionHostTest, ImpressionNavigationCommitsToErrorPage_Ignored) {
+ contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
+
+ auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
+ GURL(kConversionUrl), main_rfh());
+ navigation->SetInitiatorFrame(main_rfh());
+ navigation->set_impression(CreateValidImpression());
+ navigation->Fail(net::ERR_FAILED);
+ navigation->CommitErrorPage();
+
+ EXPECT_EQ(0u, test_manager_.num_impressions());
+}
+
+TEST_F(ConversionHostTest, ImpressionNavigationAborts_Ignored) {
+ contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
+
+ auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
+ GURL(kConversionUrl), main_rfh());
+ navigation->SetInitiatorFrame(main_rfh());
+ navigation->set_impression(CreateValidImpression());
+ navigation->AbortCommit();
+
+ EXPECT_EQ(0u, test_manager_.num_impressions());
+}
+
+TEST_F(ConversionHostTest,
+ CommittedOriginDiffersFromConversionDesintation_Ignored) {
+ contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
+
+ auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
+ GURL("https://different.com"), main_rfh());
+ navigation->SetInitiatorFrame(main_rfh());
+ navigation->set_impression(CreateValidImpression());
+ navigation->Commit();
+
+ EXPECT_EQ(0u, test_manager_.num_impressions());
+}
+
+TEST_F(ConversionHostTest,
+ ImpressionNavigation_OriginTrustworthyChecksPerformed) {
+ const char kLocalHost[] = "http://localhost";
+
+ struct {
+ std::string impression_origin;
+ std::string conversion_origin;
+ std::string reporting_origin;
+ bool impression_expected;
+ } kTestCases[] = {
+ {kLocalHost /* impression_origin */, kLocalHost /* conversion_origin */,
+ kLocalHost /* reporting_origin */, true /* impression_expected */},
+ {"http://127.0.0.1" /* impression_origin */,
+ "http://127.0.0.1" /* conversion_origin */,
+ "http://127.0.0.1" /* reporting_origin */,
+ true /* impression_expected */},
+ {kLocalHost /* impression_origin */, kLocalHost /* conversion_origin */,
+ "http://insecure.com" /* reporting_origin */,
+ false /* impression_expected */},
+ {kLocalHost /* impression_origin */,
+ "http://insecure.com" /* conversion_origin */,
+ kLocalHost /* reporting_origin */, false /* impression_expected */},
+ {"http://insecure.com" /* impression_origin */,
+ kLocalHost /* conversion_origin */, kLocalHost /* reporting_origin */,
+ false /* impression_expected */},
+ {"https://secure.com" /* impression_origin */,
+ "https://secure.com" /* conversion_origin */,
+ "https://secure.com" /* reporting_origin */,
+ true /* impression_expected */},
+ };
+
+ for (const auto& test_case : kTestCases) {
+ contents()->NavigateAndCommit(GURL(test_case.impression_origin));
+ auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
+ GURL(test_case.conversion_origin), main_rfh());
+
+ Impression impression;
+ impression.conversion_destination =
+ url::Origin::Create(GURL(test_case.conversion_origin));
+ impression.reporting_origin =
+ url::Origin::Create(GURL(test_case.reporting_origin));
+ navigation->set_impression(impression);
+ navigation->SetInitiatorFrame(main_rfh());
+ navigation->Commit();
+
+ EXPECT_EQ(test_case.impression_expected, test_manager_.num_impressions())
+ << "For test case: " << test_case.impression_origin << " | "
+ << test_case.conversion_origin << " | " << test_case.reporting_origin;
+ test_manager_.Reset();
+ }
}
} // namespace content
diff --git a/chromium/content/browser/conversions/conversion_internals.mojom b/chromium/content/browser/conversions/conversion_internals.mojom
new file mode 100644
index 00000000000..8bfdeca562c
--- /dev/null
+++ b/chromium/content/browser/conversions/conversion_internals.mojom
@@ -0,0 +1,54 @@
+// 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.
+
+module mojom;
+
+import "url/mojom/origin.mojom";
+
+// Struct containing stored data that will be sent in a future conversion
+// report.
+struct WebUIConversionReport {
+ string impression_data;
+ string conversion_data;
+ url.mojom.Origin conversion_origin;
+ url.mojom.Origin reporting_origin;
+ double report_time;
+ int32 attribution_credit;
+};
+
+// Struct representing a stored impression that will be displayed by WebUI.
+struct WebUIImpression {
+ string impression_data;
+ url.mojom.Origin impression_origin;
+ url.mojom.Origin conversion_destination;
+ url.mojom.Origin reporting_origin;
+ double impression_time;
+ double expiry_time;
+};
+
+// Mojo interface used for communication between a WebUI and the storage layer
+// for conversion measurement.
+interface ConversionInternalsHandler {
+ // Returns whether conversion measurement is enabled in the browisng context
+ // the WebUI is in.
+ IsMeasurementEnabled() => (bool enabled);
+
+ // Returns all active impressions that are persisted in storage. This does
+ // not include expired impressions, or impressions that can no longer convert
+ // due to reaching policy limits.
+ GetActiveImpressions() => (array<WebUIImpression> impressions);
+
+ // Returns all reports contained in storage, including those that are actively
+ // being sent.
+ GetPendingReports() => (array<WebUIConversionReport> reports);
+
+ // Sends all stored reports, ignoring delay, returning when the
+ // operation has been completed and all reports have been cleared from
+ // storage.
+ SendPendingReports() => ();
+
+ // Deletes all persisted data for the Conversion API, returning when the
+ // operation has been completed.
+ ClearStorage() => ();
+}; \ No newline at end of file
diff --git a/chromium/content/browser/conversions/conversion_internals_browsertest.cc b/chromium/content/browser/conversions/conversion_internals_browsertest.cc
new file mode 100644
index 00000000000..9f88538a1de
--- /dev/null
+++ b/chromium/content/browser/conversions/conversion_internals_browsertest.cc
@@ -0,0 +1,284 @@
+// 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_internals_ui.h"
+
+#include "base/optional.h"
+#include "base/time/time.h"
+#include "content/browser/conversions/conversion_manager.h"
+#include "content/browser/conversions/conversion_report.h"
+#include "content/browser/conversions/conversion_test_utils.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/browser/web_ui.h"
+#include "content/public/browser/web_ui_controller.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "content/public/test/content_browser_test.h"
+#include "content/public/test/content_browser_test_utils.h"
+#include "content/shell/browser/shell.h"
+
+namespace content {
+
+namespace {
+
+const char kConversionInternalsUrl[] = "chrome://conversion-internals/";
+
+const base::string16 kCompleteTitle = base::ASCIIToUTF16("Complete");
+
+} // namespace
+
+class ConversionInternalsWebUiBrowserTest : public ContentBrowserTest {
+ public:
+ ConversionInternalsWebUiBrowserTest() = default;
+
+ void ClickRefreshButton() {
+ EXPECT_TRUE(ExecJsInWebUI("document.getElementById('refresh').click();"));
+ }
+
+ // Executing javascript in the WebUI requires using an isolated world in which
+ // to execute the script because WebUI has a default CSP policy denying
+ // "eval()", which is what EvalJs uses under the hood.
+ bool ExecJsInWebUI(std::string script) {
+ return ExecJs(shell()->web_contents()->GetMainFrame(), script,
+ EXECUTE_SCRIPT_DEFAULT_OPTIONS, 1 /* world_id */);
+ }
+
+ void OverrideWebUIConversionManager(ConversionManager* manager) {
+ content::WebUI* web_ui = shell()->web_contents()->GetWebUI();
+
+ // Performs a safe downcast to the concrete ConversionInternalsUI subclass.
+ ConversionInternalsUI* conversion_internals_ui =
+ web_ui ? web_ui->GetController()->GetAs<ConversionInternalsUI>()
+ : nullptr;
+ EXPECT_TRUE(conversion_internals_ui);
+ conversion_internals_ui->SetConversionManagerProviderForTesting(
+ std::make_unique<TestManagerProvider>(manager));
+ }
+
+ // Registers a mutation observer that sets the window title to |title| when
+ // the report table is empty.
+ void SetTitleOnReportsTableEmpty(const base::string16& title) {
+ const std::string kObserveEmptyReportsTableScript = R"(
+ let table = document.getElementById("report-table-body");
+ let obs = new MutationObserver(() => {
+ if (table.children.length === 1 &&
+ table.children[0].children[0].innerText === "No pending reports.") {
+ document.title = $1;
+ }
+ });
+ obs.observe(table, {'childList': true});)";
+ EXPECT_TRUE(
+ ExecJsInWebUI(JsReplace(kObserveEmptyReportsTableScript, title)));
+ }
+};
+
+IN_PROC_BROWSER_TEST_F(ConversionInternalsWebUiBrowserTest,
+ NavigationUrl_ResolvedToWebUI) {
+ EXPECT_TRUE(NavigateToURL(shell(), GURL(kConversionInternalsUrl)));
+
+ // Execute script to ensure the page has loaded correctly, executing similarly
+ // to ExecJsInWebUI().
+ EXPECT_EQ(
+ true,
+ EvalJs(shell()->web_contents()->GetMainFrame(),
+ "document.body.innerHTML.search('Conversion Internals') >= 0;",
+ EXECUTE_SCRIPT_DEFAULT_OPTIONS, 1 /* world_id */));
+}
+
+IN_PROC_BROWSER_TEST_F(ConversionInternalsWebUiBrowserTest,
+ WebUIShownWithManager_MeasurementConsideredEnabled) {
+ EXPECT_TRUE(NavigateToURL(shell(), GURL(kConversionInternalsUrl)));
+
+ TestConversionManager manager;
+ OverrideWebUIConversionManager(&manager);
+
+ // Create a mutation observer to wait for the content to render to the dom.
+ // Waiting on calls to TestConversionManager is not sufficient because the
+ // results are returned in promises.
+ std::string wait_script = R"(
+ let status = document.getElementById("feature-status-content");
+ let obs = new MutationObserver(() => {
+ if (status.innerText.trim() === "enabled") {
+ document.title = $1;
+ }
+ });
+ obs.observe(status, {'childList': true, 'characterData': true});)";
+ EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
+
+ TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
+ ClickRefreshButton();
+ EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
+}
+
+IN_PROC_BROWSER_TEST_F(
+ ConversionInternalsWebUiBrowserTest,
+ WebUIShownWithNoActiveImpression_NoImpressionsDisplayed) {
+ EXPECT_TRUE(NavigateToURL(shell(), GURL(kConversionInternalsUrl)));
+
+ TestConversionManager manager;
+ OverrideWebUIConversionManager(&manager);
+
+ std::string wait_script = R"(
+ let table = document.getElementById("impression-table-body");
+ let obs = new MutationObserver(() => {
+ if (table.children.length === 1 &&
+ table.children[0].children[0].innerText ===
+ "No active impressions.") {
+ document.title = $1;
+ }
+ });
+ obs.observe(table, {'childList': true});)";
+ EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
+
+ TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
+ ClickRefreshButton();
+ EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
+}
+
+IN_PROC_BROWSER_TEST_F(ConversionInternalsWebUiBrowserTest,
+ WebUIShownWithActiveImpression_ImpressionsDisplayed) {
+ EXPECT_TRUE(NavigateToURL(shell(), GURL(kConversionInternalsUrl)));
+
+ TestConversionManager manager;
+ manager.SetActiveImpressionsForWebUI(
+ {ImpressionBuilder(base::Time::Now()).SetData("100").Build(),
+ ImpressionBuilder(base::Time::Now()).Build()});
+ OverrideWebUIConversionManager(&manager);
+
+ std::string wait_script = R"(
+ let table = document.getElementById("impression-table-body");
+ let obs = new MutationObserver(() => {
+ if (table.children.length === 2 &&
+ table.children[0].children[0].innerText === "0x100") {
+ document.title = $1;
+ }
+ });
+ obs.observe(table, {'childList': true});)";
+ EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
+
+ TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
+ ClickRefreshButton();
+ EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
+}
+
+IN_PROC_BROWSER_TEST_F(ConversionInternalsWebUiBrowserTest,
+ WebUIShownWithNoReports_NoReportsDisplayed) {
+ EXPECT_TRUE(NavigateToURL(shell(), GURL(kConversionInternalsUrl)));
+
+ TestConversionManager manager;
+ OverrideWebUIConversionManager(&manager);
+
+ TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
+ SetTitleOnReportsTableEmpty(kCompleteTitle);
+ ClickRefreshButton();
+ EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
+}
+
+IN_PROC_BROWSER_TEST_F(ConversionInternalsWebUiBrowserTest,
+ WebUIShownWithPendingReports_ReportsDisplayed) {
+ EXPECT_TRUE(NavigateToURL(shell(), GURL(kConversionInternalsUrl)));
+
+ TestConversionManager manager;
+ ConversionReport report(
+ ImpressionBuilder(base::Time::Now()).SetData("100").Build(),
+ "7" /* conversion_data */, base::Time::Now() /* report_time */,
+ 1 /* conversion_id */);
+ manager.SetReportsForWebUI({report});
+ OverrideWebUIConversionManager(&manager);
+
+ std::string wait_script = R"(
+ let table = document.getElementById("report-table-body");
+ let obs = new MutationObserver(() => {
+ if (table.children.length === 1 &&
+ table.children[0].children[1].innerText === "0x7") {
+ document.title = $1;
+ }
+ });
+ obs.observe(table, {'childList': true});)";
+ EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
+
+ TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
+ ClickRefreshButton();
+ EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
+}
+
+IN_PROC_BROWSER_TEST_F(ConversionInternalsWebUiBrowserTest,
+ WebUIWithPendingReportsClearStorage_ReportsRemoved) {
+ EXPECT_TRUE(NavigateToURL(shell(), GURL(kConversionInternalsUrl)));
+
+ TestConversionManager manager;
+ ConversionReport report(
+ ImpressionBuilder(base::Time::Now()).SetData("100").Build(),
+ "7" /* conversion_data */, base::Time::Now() /* report_time */,
+ 1 /* conversion_id */);
+ manager.SetReportsForWebUI({report});
+ OverrideWebUIConversionManager(&manager);
+
+ std::string wait_script = R"(
+ let table = document.getElementById("report-table-body");
+ let obs = new MutationObserver(() => {
+ if (table.children.length === 1 &&
+ table.children[0].children[1].innerText === "0x7") {
+ document.title = $1;
+ }
+ });
+ obs.observe(table, {'childList': true});)";
+ EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
+
+ // Wait for the table to rendered.
+ TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
+ ClickRefreshButton();
+ EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
+
+ // Click the clear storage button and expect that the report table is emptied.
+ const base::string16 kDeleteTitle = base::ASCIIToUTF16("Delete");
+ TitleWatcher delete_title_watcher(shell()->web_contents(), kDeleteTitle);
+ SetTitleOnReportsTableEmpty(kDeleteTitle);
+
+ // Click the button.
+ EXPECT_TRUE(ExecJsInWebUI("document.getElementById('clear-data').click();"));
+ EXPECT_EQ(kDeleteTitle, delete_title_watcher.WaitAndGetTitle());
+}
+
+// TODO(johnidel): Use a real ConversionManager here and verify that the reports
+// are actually sent.
+IN_PROC_BROWSER_TEST_F(ConversionInternalsWebUiBrowserTest,
+ WebUISendReports_ReportsRemoved) {
+ EXPECT_TRUE(NavigateToURL(shell(), GURL(kConversionInternalsUrl)));
+
+ TestConversionManager manager;
+ ConversionReport report(
+ ImpressionBuilder(base::Time::Now()).SetData("100").Build(),
+ "7" /* conversion_data */, base::Time::Now() /* report_time */,
+ 1 /* conversion_id */);
+ manager.SetReportsForWebUI({report});
+ OverrideWebUIConversionManager(&manager);
+
+ std::string wait_script = R"(
+ let table = document.getElementById("report-table-body");
+ let obs = new MutationObserver(() => {
+ if (table.children.length === 1 &&
+ table.children[0].children[1].innerText === "0x7") {
+ document.title = $1;
+ }
+ });
+ obs.observe(table, {'childList': true});)";
+ EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
+
+ // Wait for the table to rendered.
+ TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
+ ClickRefreshButton();
+ EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
+
+ // Click the send reports button and expect that the report table is emptied.
+ const base::string16 kSentTitle = base::ASCIIToUTF16("Sent");
+ TitleWatcher sent_title_watcher(shell()->web_contents(), kSentTitle);
+ SetTitleOnReportsTableEmpty(kSentTitle);
+
+ EXPECT_TRUE(
+ ExecJsInWebUI("document.getElementById('send-reports').click();"));
+ EXPECT_EQ(kSentTitle, sent_title_watcher.WaitAndGetTitle());
+}
+
+} // namespace content
diff --git a/chromium/content/browser/conversions/conversion_internals_handler_impl.cc b/chromium/content/browser/conversions/conversion_internals_handler_impl.cc
new file mode 100644
index 00000000000..b1ec072a119
--- /dev/null
+++ b/chromium/content/browser/conversions/conversion_internals_handler_impl.cc
@@ -0,0 +1,129 @@
+// 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_internals_handler_impl.h"
+
+#include <utility>
+#include <vector>
+
+#include "base/bind_helpers.h"
+#include "base/callback.h"
+#include "base/time/time.h"
+#include "content/browser/conversions/conversion_manager_impl.h"
+#include "content/browser/conversions/conversion_report.h"
+#include "content/browser/conversions/storable_impression.h"
+#include "content/browser/storage_partition_impl.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/browser/web_ui.h"
+
+namespace content {
+
+namespace {
+
+void ForwardImpressionsToWebUI(
+ ::mojom::ConversionInternalsHandler::GetActiveImpressionsCallback
+ web_ui_callback,
+ std::vector<StorableImpression> stored_impressions) {
+ std::vector<::mojom::WebUIImpressionPtr> web_ui_impressions;
+ web_ui_impressions.reserve(stored_impressions.size());
+
+ for (const StorableImpression& impression : stored_impressions) {
+ web_ui_impressions.push_back(::mojom::WebUIImpression::New(
+ impression.impression_data(), impression.impression_origin(),
+ impression.conversion_origin(), impression.reporting_origin(),
+ impression.impression_time().ToJsTime(),
+ impression.expiry_time().ToJsTime()));
+ }
+
+ std::move(web_ui_callback).Run(std::move(web_ui_impressions));
+}
+
+void ForwardReportsToWebUI(
+ ::mojom::ConversionInternalsHandler::GetPendingReportsCallback
+ web_ui_callback,
+ std::vector<ConversionReport> stored_reports) {
+ std::vector<::mojom::WebUIConversionReportPtr> web_ui_reports;
+ web_ui_reports.reserve(stored_reports.size());
+
+ for (const ConversionReport& report : stored_reports) {
+ web_ui_reports.push_back(::mojom::WebUIConversionReport::New(
+ report.impression.impression_data(), report.conversion_data,
+ report.impression.conversion_origin(),
+ report.impression.reporting_origin(), report.report_time.ToJsTime(),
+ report.attribution_credit));
+ }
+ std::move(web_ui_callback).Run(std::move(web_ui_reports));
+}
+
+} // namespace
+
+ConversionInternalsHandlerImpl::ConversionInternalsHandlerImpl(
+ WebUI* web_ui,
+ mojo::PendingReceiver<::mojom::ConversionInternalsHandler> receiver)
+ : web_ui_(web_ui),
+ manager_provider_(std::make_unique<ConversionManagerProviderImpl>()),
+ receiver_(this, std::move(receiver)) {}
+
+ConversionInternalsHandlerImpl::~ConversionInternalsHandlerImpl() = default;
+
+void ConversionInternalsHandlerImpl::IsMeasurementEnabled(
+ ::mojom::ConversionInternalsHandler::IsMeasurementEnabledCallback
+ callback) {
+ bool measurement_enabled =
+ manager_provider_->GetManager(web_ui_->GetWebContents());
+ std::move(callback).Run(measurement_enabled);
+}
+
+void ConversionInternalsHandlerImpl::GetActiveImpressions(
+ ::mojom::ConversionInternalsHandler::GetActiveImpressionsCallback
+ callback) {
+ if (ConversionManager* manager =
+ manager_provider_->GetManager(web_ui_->GetWebContents())) {
+ manager->GetActiveImpressionsForWebUI(
+ base::BindOnce(&ForwardImpressionsToWebUI, std::move(callback)));
+ } else {
+ std::move(callback).Run({});
+ }
+}
+
+void ConversionInternalsHandlerImpl::GetPendingReports(
+ ::mojom::ConversionInternalsHandler::GetPendingReportsCallback callback) {
+ if (ConversionManager* manager =
+ manager_provider_->GetManager(web_ui_->GetWebContents())) {
+ manager->GetReportsForWebUI(
+ base::BindOnce(&ForwardReportsToWebUI, std::move(callback)),
+ base::Time::Max());
+ } else {
+ std::move(callback).Run({});
+ }
+}
+
+void ConversionInternalsHandlerImpl::SendPendingReports(
+ ::mojom::ConversionInternalsHandler::SendPendingReportsCallback callback) {
+ if (ConversionManager* manager =
+ manager_provider_->GetManager(web_ui_->GetWebContents())) {
+ manager->SendReportsForWebUI(std::move(callback));
+ } else {
+ std::move(callback).Run();
+ }
+}
+
+void ConversionInternalsHandlerImpl::ClearStorage(
+ ::mojom::ConversionInternalsHandler::ClearStorageCallback callback) {
+ if (ConversionManager* manager =
+ manager_provider_->GetManager(web_ui_->GetWebContents())) {
+ manager->ClearData(base::Time::Min(), base::Time::Max(),
+ base::NullCallback(), std::move(callback));
+ } else {
+ std::move(callback).Run();
+ }
+}
+
+void ConversionInternalsHandlerImpl::SetConversionManagerProviderForTesting(
+ std::unique_ptr<ConversionManager::Provider> manager_provider) {
+ manager_provider_ = std::move(manager_provider);
+}
+
+} // namespace content
diff --git a/chromium/content/browser/conversions/conversion_internals_handler_impl.h b/chromium/content/browser/conversions/conversion_internals_handler_impl.h
new file mode 100644
index 00000000000..76f621308a8
--- /dev/null
+++ b/chromium/content/browser/conversions/conversion_internals_handler_impl.h
@@ -0,0 +1,56 @@
+// 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_INTERNALS_HANDLER_IMPL_H_
+#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_INTERNALS_HANDLER_IMPL_H_
+
+#include "content/browser/conversions/conversion_internals.mojom.h"
+#include "content/browser/conversions/conversion_manager.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+
+namespace content {
+
+class WebUI;
+
+// Implements the mojo endpoint for the Conversion WebUI which proxies calls to
+// the ConversionManager to get information about stored conversion data. Owned
+// by ConversionInternalsUI.
+class ConversionInternalsHandlerImpl
+ : public ::mojom::ConversionInternalsHandler {
+ public:
+ ConversionInternalsHandlerImpl(
+ WebUI* web_ui,
+ mojo::PendingReceiver<::mojom::ConversionInternalsHandler> receiver);
+ ~ConversionInternalsHandlerImpl() override;
+
+ // mojom::ConversionInternalsHandler overrides:
+ void IsMeasurementEnabled(
+ ::mojom::ConversionInternalsHandler::IsMeasurementEnabledCallback
+ callback) override;
+ void GetActiveImpressions(
+ ::mojom::ConversionInternalsHandler::GetActiveImpressionsCallback
+ callback) override;
+ void GetPendingReports(
+ ::mojom::ConversionInternalsHandler::GetPendingReportsCallback callback)
+ override;
+ void SendPendingReports(
+ ::mojom::ConversionInternalsHandler::SendPendingReportsCallback callback)
+ override;
+ void ClearStorage(::mojom::ConversionInternalsHandler::ClearStorageCallback
+ callback) override;
+
+ void SetConversionManagerProviderForTesting(
+ std::unique_ptr<ConversionManager::Provider> manager_provider);
+
+ private:
+ WebUI* web_ui_;
+ std::unique_ptr<ConversionManager::Provider> manager_provider_;
+
+ mojo::Receiver<::mojom::ConversionInternalsHandler> receiver_;
+};
+
+} // namespace content
+
+#endif // CONTENT_BROWSER_CONVERSIONS_CONVERSION_INTERNALS_HANDLER_IMPL_H_
diff --git a/chromium/content/browser/conversions/conversion_internals_ui.cc b/chromium/content/browser/conversions/conversion_internals_ui.cc
new file mode 100644
index 00000000000..20306d5f62c
--- /dev/null
+++ b/chromium/content/browser/conversions/conversion_internals_ui.cc
@@ -0,0 +1,59 @@
+// 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_internals_ui.h"
+
+#include "content/browser/conversions/conversion_internals_handler_impl.h"
+#include "content/browser/frame_host/render_frame_host_impl.h"
+#include "content/grit/dev_ui_content_resources.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/browser/web_ui.h"
+#include "content/public/browser/web_ui_data_source.h"
+#include "content/public/common/bindings_policy.h"
+#include "content/public/common/url_constants.h"
+
+namespace content {
+
+ConversionInternalsUI::ConversionInternalsUI(WebUI* web_ui)
+ : WebUIController(web_ui) {
+ // Initialize the UI with no bindings. Mojo bindings will be separately
+ // granted to frames within this WebContents.
+ web_ui->SetBindings(0);
+ WebUIDataSource* source =
+ WebUIDataSource::Create(kChromeUIConversionInternalsHost);
+
+ source->AddResourcePath("conversion_internals.mojom-lite.js",
+ IDR_CONVERSION_INTERNALS_MOJOM_JS);
+ source->AddResourcePath("conversion_internals.js",
+ IDR_CONVERSION_INTERNALS_JS);
+ source->AddResourcePath("conversion_internals.css",
+ IDR_CONVERSION_INTERNALS_CSS);
+ source->SetDefaultResource(IDR_CONVERSION_INTERNALS_HTML);
+ WebUIDataSource::Add(web_ui->GetWebContents()->GetBrowserContext(), source);
+}
+
+WEB_UI_CONTROLLER_TYPE_IMPL(ConversionInternalsUI)
+
+ConversionInternalsUI::~ConversionInternalsUI() = default;
+
+void ConversionInternalsUI::RenderFrameCreated(RenderFrameHost* rfh) {
+ // Enable the JavaScript Mojo bindings in the renderer process, so the JS
+ // code can call the Mojo APIs exposed by this WebUI.
+ static_cast<RenderFrameHostImpl*>(rfh)->EnableMojoJsBindings();
+}
+
+void ConversionInternalsUI::BindInterface(
+ mojo::PendingReceiver<::mojom::ConversionInternalsHandler> receiver) {
+ ui_handler_ = std::make_unique<ConversionInternalsHandlerImpl>(
+ web_ui(), std::move(receiver));
+}
+
+void ConversionInternalsUI::SetConversionManagerProviderForTesting(
+ std::unique_ptr<ConversionManager::Provider> manager_provider) {
+ ui_handler_->SetConversionManagerProviderForTesting(
+ std::move(manager_provider));
+}
+
+} // namespace content
diff --git a/chromium/content/browser/conversions/conversion_internals_ui.h b/chromium/content/browser/conversions/conversion_internals_ui.h
new file mode 100644
index 00000000000..88fbdf797c3
--- /dev/null
+++ b/chromium/content/browser/conversions/conversion_internals_ui.h
@@ -0,0 +1,47 @@
+// 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_INTERNALS_UI_H_
+#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_INTERNALS_UI_H_
+
+#include <memory>
+
+#include "content/browser/conversions/conversion_internals.mojom.h"
+#include "content/browser/conversions/conversion_manager.h"
+#include "content/common/content_export.h"
+#include "content/public/browser/web_contents_observer.h"
+#include "content/public/browser/web_ui_controller.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+
+namespace content {
+
+class ConversionInternalsHandlerImpl;
+
+// WebUI which handles serving the chrome://conversion-internals page.
+class CONTENT_EXPORT ConversionInternalsUI : public WebUIController,
+ public WebContentsObserver {
+ public:
+ explicit ConversionInternalsUI(WebUI* web_ui);
+ ConversionInternalsUI(const ConversionInternalsUI& other) = delete;
+ ConversionInternalsUI& operator=(const ConversionInternalsUI& other) = delete;
+ ~ConversionInternalsUI() override;
+
+ // WebContentsObserver:
+ void RenderFrameCreated(RenderFrameHost* render_frame_host) override;
+
+ void BindInterface(
+ mojo::PendingReceiver<::mojom::ConversionInternalsHandler> receiver);
+
+ void SetConversionManagerProviderForTesting(
+ std::unique_ptr<ConversionManager::Provider> manager_provider);
+
+ private:
+ std::unique_ptr<ConversionInternalsHandlerImpl> ui_handler_;
+
+ WEB_UI_CONTROLLER_TYPE_DECL();
+};
+
+} // namespace content
+
+#endif // CONTENT_BROWSER_CONVERSIONS_CONVERSION_INTERNALS_UI_H_
diff --git a/chromium/content/browser/conversions/conversion_manager.cc b/chromium/content/browser/conversions/conversion_manager.cc
deleted file mode 100644
index 2e358851a61..00000000000
--- a/chromium/content/browser/conversions/conversion_manager.cc
+++ /dev/null
@@ -1,75 +0,0 @@
-// 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_manager.h"
-
-#include <memory>
-
-#include "base/bind.h"
-#include "base/task_runner_util.h"
-#include "base/time/default_clock.h"
-#include "content/browser/conversions/conversion_storage_sql.h"
-
-namespace content {
-
-ConversionManager::ConversionManager(
- const base::FilePath& user_data_directory,
- scoped_refptr<base::SequencedTaskRunner> task_runner)
- : storage_task_runner_(std::move(task_runner)),
- clock_(base::DefaultClock::GetInstance()),
- storage_(new ConversionStorageSql(user_data_directory, this, clock_),
- base::OnTaskRunnerDeleter(storage_task_runner_)),
- conversion_policy_(std::make_unique<ConversionPolicy>()),
- weak_factory_(this) {
- // Unretained is safe because any task to delete |storage_| will be posted
- // after this one.
- base::PostTaskAndReplyWithResult(
- storage_task_runner_.get(), FROM_HERE,
- base::BindOnce(&ConversionStorage::Initialize,
- base::Unretained(storage_.get())),
- base::BindOnce(&ConversionManager::OnInitCompleted,
- weak_factory_.GetWeakPtr()));
-}
-
-ConversionManager::~ConversionManager() = default;
-
-void ConversionManager::HandleConversion(const StorableConversion& conversion) {
- if (!storage_)
- return;
-
- // TODO(https://crbug.com/1043345): Add UMA for the number of conversions we
- // are logging to storage, and the number of new reports logged to storage.
- // Unretained is safe because any task to delete |storage_| will be posted
- // after this one.
- storage_task_runner_.get()->PostTask(
- FROM_HERE,
- base::BindOnce(
- base::IgnoreResult(
- &ConversionStorage::MaybeCreateAndStoreConversionReports),
- base::Unretained(storage_.get()), conversion));
-}
-
-const ConversionPolicy& ConversionManager::GetConversionPolicy() const {
- return *conversion_policy_;
-}
-
-void ConversionManager::ProcessNewConversionReports(
- std::vector<ConversionReport>* reports) {
- for (ConversionReport& report : *reports) {
- report.report_time = conversion_policy_->GetReportTimeForConversion(report);
- }
-
- conversion_policy_->AssignAttributionCredits(reports);
-}
-
-int ConversionManager::GetMaxConversionsPerImpression() const {
- return conversion_policy_->GetMaxConversionsPerImpression();
-}
-
-void ConversionManager::OnInitCompleted(bool success) {
- if (!success)
- storage_.reset();
-}
-
-} // namespace content
diff --git a/chromium/content/browser/conversions/conversion_manager.h b/chromium/content/browser/conversions/conversion_manager.h
index fe132c41e2c..ca1e1a041f8 100644
--- a/chromium/content/browser/conversions/conversion_manager.h
+++ b/chromium/content/browser/conversions/conversion_manager.h
@@ -5,68 +5,76 @@
#ifndef CONTENT_BROWSER_CONVERSIONS_CONVERSION_MANAGER_H_
#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_MANAGER_H_
-#include <memory>
+#include <vector>
-#include "base/files/file_path.h"
-#include "base/macros.h"
-#include "base/memory/scoped_refptr.h"
-#include "base/memory/weak_ptr.h"
-#include "base/sequenced_task_runner.h"
+#include "base/callback.h"
+#include "base/callback_forward.h"
#include "content/browser/conversions/conversion_policy.h"
-#include "content/browser/conversions/conversion_storage.h"
+#include "content/browser/conversions/conversion_report.h"
#include "content/browser/conversions/storable_conversion.h"
-
-namespace base {
-class Clock;
-} // namespace base
+#include "content/browser/conversions/storable_impression.h"
+#include "content/common/content_export.h"
+#include "url/origin.h"
namespace content {
-class ConversionStorage;
+class WebContents;
-// UI thread class that manages the lifetime of the underlying conversion
-// storage. Owned by the storage partition.
-class ConversionManager : public ConversionStorage::Delegate {
+// Interface that mediates data flow between the network, storage layer, and
+// blink.
+class CONTENT_EXPORT ConversionManager {
public:
- // |storage_task_runner| should run with base::TaskPriority::BEST_EFFORT.
- ConversionManager(
- const base::FilePath& user_data_directory,
- scoped_refptr<base::SequencedTaskRunner> storage_task_runner);
- ConversionManager(const ConversionManager& other) = delete;
- ConversionManager& operator=(const ConversionManager& other) = delete;
- ~ConversionManager() override;
+ // Provides access to a ConversionManager implementation. This layer of
+ // abstraction is to allow tests to mock out the ConversionManager without
+ // injecting a manager explicitly.
+ class Provider {
+ public:
+ virtual ~Provider() = default;
+
+ // Gets the ConversionManager that should be used for handling conversions
+ // that occur in the given |web_contents|. Returns nullptr if conversion
+ // measurement is not enabled in the given |web_contents|, e.g. when the
+ // browser context is off the record.
+ virtual ConversionManager* GetManager(WebContents* web_contents) const = 0;
+ };
+ virtual ~ConversionManager() = default;
+
+ // Persists the given |impression| to storage. Called when a navigation
+ // originating from an impression tag finishes.
+ virtual void HandleImpression(const StorableImpression& impression) = 0;
// Process a newly registered conversion. Will create and log any new
// conversion reports to storage.
- void HandleConversion(const StorableConversion& conversion);
-
- const ConversionPolicy& GetConversionPolicy() const;
-
- private:
- // ConversionStorageDelegate
- void ProcessNewConversionReports(
- std::vector<ConversionReport>* reports) override;
- int GetMaxConversionsPerImpression() const override;
-
- void OnInitCompleted(bool success);
-
- // Task runner used to perform operations on |storage_|. Runs with
- // base::TaskPriority::BEST_EFFORT.
- scoped_refptr<base::SequencedTaskRunner> storage_task_runner_;
-
- base::Clock* clock_;
-
- // ConversionStorage instance which is scoped to lifetime of
- // |storage_task_runner_|. |storage_| should be accessed by calling
- // base::PostTask with |storage_task_runner_|, and should not be accessed
- // directly.
- std::unique_ptr<ConversionStorage, base::OnTaskRunnerDeleter> storage_;
-
- // Policy used for controlling API configurations such as reporting and
- // attribution models. Unique ptr so it can be overridden for testing.
- std::unique_ptr<ConversionPolicy> conversion_policy_;
-
- base::WeakPtrFactory<ConversionManager> weak_factory_;
+ virtual void HandleConversion(const StorableConversion& conversion) = 0;
+
+ // Get all impressions that are currently stored in this partition. Used for
+ // populating WebUI.
+ virtual void GetActiveImpressionsForWebUI(
+ base::OnceCallback<void(std::vector<StorableImpression>)> callback) = 0;
+
+ // Get all pending reports that are currently stored in this partition. Used
+ // for populating WebUI.
+ virtual void GetReportsForWebUI(
+ base::OnceCallback<void(std::vector<ConversionReport>)> callback,
+ base::Time max_report_time) = 0;
+
+ // Sends all pending reports immediately, and runs |done| once they have all
+ // been sent.
+ virtual void SendReportsForWebUI(base::OnceClosure done) = 0;
+
+ // Returns the ConversionPolicy that is used to control API policies such
+ // as noise.
+ virtual const ConversionPolicy& GetConversionPolicy() const = 0;
+
+ // Deletes all data in storage for URLs matching |filter|, between
+ // |delete_begin| and |delete_end| time.
+ //
+ // If |filter| is null, then consider all origins in storage as matching.
+ virtual void ClearData(
+ base::Time delete_begin,
+ base::Time delete_end,
+ base::RepeatingCallback<bool(const url::Origin&)> filter,
+ base::OnceClosure done) = 0;
};
} // namespace content
diff --git a/chromium/content/browser/conversions/conversion_manager_impl.cc b/chromium/content/browser/conversions/conversion_manager_impl.cc
new file mode 100644
index 00000000000..4f3f9ca7e5a
--- /dev/null
+++ b/chromium/content/browser/conversions/conversion_manager_impl.cc
@@ -0,0 +1,282 @@
+// 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_manager_impl.h"
+
+#include <utility>
+
+#include "base/barrier_closure.h"
+#include "base/bind.h"
+#include "base/command_line.h"
+#include "base/memory/ptr_util.h"
+#include "base/task_runner_util.h"
+#include "base/time/default_clock.h"
+#include "content/browser/conversions/conversion_reporter_impl.h"
+#include "content/browser/conversions/conversion_storage_delegate_impl.h"
+#include "content/browser/conversions/conversion_storage_sql.h"
+#include "content/browser/storage_partition_impl.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/storage_partition.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/common/content_switches.h"
+
+namespace content {
+
+const constexpr base::TimeDelta kConversionManagerQueueReportsInterval =
+ base::TimeDelta::FromMinutes(30);
+
+ConversionManager* ConversionManagerProviderImpl::GetManager(
+ WebContents* web_contents) const {
+ return static_cast<StoragePartitionImpl*>(
+ BrowserContext::GetDefaultStoragePartition(
+ web_contents->GetBrowserContext()))
+ ->GetConversionManager();
+}
+
+// static
+std::unique_ptr<ConversionManagerImpl> ConversionManagerImpl::CreateForTesting(
+ std::unique_ptr<ConversionReporter> reporter,
+ std::unique_ptr<ConversionPolicy> policy,
+ const base::Clock* clock,
+ const base::FilePath& user_data_directory,
+ scoped_refptr<base::SequencedTaskRunner> storage_task_runner) {
+ return base::WrapUnique<ConversionManagerImpl>(new ConversionManagerImpl(
+ std::move(reporter), std::move(policy), clock, user_data_directory,
+ std::move(storage_task_runner)));
+}
+
+ConversionManagerImpl::ConversionManagerImpl(
+ StoragePartition* storage_partition,
+ const base::FilePath& user_data_directory,
+ scoped_refptr<base::SequencedTaskRunner> task_runner)
+ : ConversionManagerImpl(
+ std::make_unique<ConversionReporterImpl>(
+ storage_partition,
+ base::DefaultClock::GetInstance()),
+ std::make_unique<ConversionPolicy>(
+ base::CommandLine::ForCurrentProcess()->HasSwitch(
+ switches::kConversionsDebugMode)),
+ base::DefaultClock::GetInstance(),
+ user_data_directory,
+ std::move(task_runner)) {}
+
+ConversionManagerImpl::ConversionManagerImpl(
+ std::unique_ptr<ConversionReporter> reporter,
+ std::unique_ptr<ConversionPolicy> policy,
+ const base::Clock* clock,
+ const base::FilePath& user_data_directory,
+ scoped_refptr<base::SequencedTaskRunner> storage_task_runner)
+ : debug_mode_(base::CommandLine::ForCurrentProcess()->HasSwitch(
+ switches::kConversionsDebugMode)),
+ storage_task_runner_(std::move(storage_task_runner)),
+ clock_(clock),
+ reporter_(std::move(reporter)),
+ storage_(new ConversionStorageSql(
+ user_data_directory,
+ std::make_unique<ConversionStorageDelegateImpl>(debug_mode_),
+ clock_),
+ base::OnTaskRunnerDeleter(storage_task_runner_)),
+ conversion_policy_(std::move(policy)),
+ weak_factory_(this) {
+ // Unretained is safe because any task to delete |storage_| will be posted
+ // after this one.
+ base::PostTaskAndReplyWithResult(
+ storage_task_runner_.get(), FROM_HERE,
+ base::BindOnce(&ConversionStorage::Initialize,
+ base::Unretained(storage_.get())),
+ base::BindOnce(&ConversionManagerImpl::OnInitCompleted,
+ weak_factory_.GetWeakPtr()));
+}
+
+ConversionManagerImpl::~ConversionManagerImpl() = default;
+
+void ConversionManagerImpl::HandleImpression(
+ const StorableImpression& impression) {
+ if (!storage_)
+ return;
+
+ // Add the impression to storage.
+ storage_task_runner_->PostTask(
+ FROM_HERE, base::BindOnce(&ConversionStorage::StoreImpression,
+ base::Unretained(storage_.get()), impression));
+}
+
+void ConversionManagerImpl::HandleConversion(
+ const StorableConversion& conversion) {
+ if (!storage_)
+ return;
+
+ // TODO(https://crbug.com/1043345): Add UMA for the number of conversions we
+ // are logging to storage, and the number of new reports logged to storage.
+ // Unretained is safe because any task to delete |storage_| will be posted
+ // after this one.
+ storage_task_runner_.get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(
+ base::IgnoreResult(
+ &ConversionStorage::MaybeCreateAndStoreConversionReports),
+ base::Unretained(storage_.get()), conversion));
+
+ // If we are running in debug mode, we should also schedule a task to
+ // gather and send any new reports.
+ if (debug_mode_)
+ GetAndQueueReportsForNextInterval();
+}
+
+void ConversionManagerImpl::GetActiveImpressionsForWebUI(
+ base::OnceCallback<void(std::vector<StorableImpression>)> callback) {
+ // Unretained is safe because any task to delete |storage_| will be posted
+ // after this one because |storage_| uses base::OnTaskRunnerDeleter.
+ base::PostTaskAndReplyWithResult(
+ storage_task_runner_.get(), FROM_HERE,
+ base::BindOnce(&ConversionStorage::GetActiveImpressions,
+ base::Unretained(storage_.get())),
+ std::move(callback));
+}
+
+void ConversionManagerImpl::GetReportsForWebUI(
+ base::OnceCallback<void(std::vector<ConversionReport>)> callback,
+ base::Time max_report_time) {
+ GetAndHandleReports(std::move(callback), max_report_time);
+}
+
+void ConversionManagerImpl::SendReportsForWebUI(base::OnceClosure done) {
+ GetAndHandleReports(
+ base::BindOnce(&ConversionManagerImpl::HandleReportsSentFromWebUI,
+ weak_factory_.GetWeakPtr(), std::move(done)),
+ base::Time::Max());
+}
+
+const ConversionPolicy& ConversionManagerImpl::GetConversionPolicy() const {
+ return *conversion_policy_;
+}
+
+void ConversionManagerImpl::ClearData(
+ base::Time delete_begin,
+ base::Time delete_end,
+ base::RepeatingCallback<bool(const url::Origin&)> filter,
+ base::OnceClosure done) {
+ storage_task_runner_->PostTaskAndReply(
+ FROM_HERE,
+ base::BindOnce(&ConversionStorage::ClearData,
+ base::Unretained(storage_.get()), delete_begin, delete_end,
+ std::move(filter)),
+ std::move(done));
+}
+
+void ConversionManagerImpl::OnInitCompleted(bool success) {
+ if (!success) {
+ storage_.reset();
+ return;
+ }
+
+ // Once the database is loaded, get all reports that may have expired while
+ // Chrome was not running and handle these specially.
+ GetAndHandleReports(
+ base::BindOnce(&ConversionManagerImpl::HandleReportsExpiredAtStartup,
+ weak_factory_.GetWeakPtr()),
+ clock_->Now() + kConversionManagerQueueReportsInterval);
+
+ // Start a repeating timer that will fetch reports once every
+ // |kConversionManagerQueueReportsInterval| and add them to |reporter_|.
+ get_and_queue_reports_timer_.Start(
+ FROM_HERE, kConversionManagerQueueReportsInterval, this,
+ &ConversionManagerImpl::GetAndQueueReportsForNextInterval);
+}
+
+void ConversionManagerImpl::GetAndHandleReports(
+ ReportsHandlerFunc handler_function,
+ base::Time max_report_time) {
+ base::PostTaskAndReplyWithResult(
+ storage_task_runner_.get(), FROM_HERE,
+ base::BindOnce(&ConversionStorage::GetConversionsToReport,
+ base::Unretained(storage_.get()), max_report_time),
+ std::move(handler_function));
+}
+
+void ConversionManagerImpl::GetAndQueueReportsForNextInterval() {
+ // Get all the reports that will be reported in the next interval and them to
+ // the |reporter_|.
+ GetAndHandleReports(base::BindOnce(&ConversionManagerImpl::QueueReports,
+ weak_factory_.GetWeakPtr()),
+ clock_->Now() + kConversionManagerQueueReportsInterval);
+}
+
+void ConversionManagerImpl::QueueReports(
+ std::vector<ConversionReport> reports) {
+ if (!reports.empty()) {
+ // |reporter_| is owned by |this|, so base::Unretained() is safe as the
+ // reporter and callbacks will be deleted first.
+ reporter_->AddReportsToQueue(
+ std::move(reports),
+ base::BindRepeating(&ConversionManagerImpl::OnReportSent,
+ base::Unretained(this)));
+ }
+}
+
+void ConversionManagerImpl::HandleReportsExpiredAtStartup(
+ std::vector<ConversionReport> reports) {
+ // Add delay to all reports that expired while the browser was not running so
+ // they are not temporally join-able.
+ base::Time current_time = clock_->Now();
+ for (ConversionReport& report : reports) {
+ if (report.report_time > current_time)
+ continue;
+
+ base::Time updated_report_time =
+ conversion_policy_->GetReportTimeForExpiredReportAtStartup(
+ current_time);
+
+ report.extra_delay = updated_report_time - report.report_time;
+ report.report_time = updated_report_time;
+ }
+ QueueReports(std::move(reports));
+}
+
+void ConversionManagerImpl::HandleReportsSentFromWebUI(
+ base::OnceClosure done,
+ std::vector<ConversionReport> reports) {
+ if (reports.empty()) {
+ std::move(done).Run();
+ return;
+ }
+
+ // All reports should be sent immediately.
+ for (ConversionReport& report : reports) {
+ report.report_time = base::Time::Min();
+ }
+
+ // Wraps |done| so that is will run once all of the reports have finished
+ // sending.
+ base::RepeatingClosure all_reports_sent =
+ base::BarrierClosure(reports.size(), std::move(done));
+
+ // |reporter_| is owned by |this|, so base::Unretained() is safe as the
+ // reporter and callbacks will be deleted first.
+ reporter_->AddReportsToQueue(
+ std::move(reports),
+ base::BindRepeating(&ConversionManagerImpl::OnReportSentFromWebUI,
+ base::Unretained(this), std::move(all_reports_sent)));
+}
+
+void ConversionManagerImpl::OnReportSent(int64_t conversion_id) {
+ storage_task_runner_->PostTask(
+ FROM_HERE,
+ base::BindOnce(base::IgnoreResult(&ConversionStorage::DeleteConversion),
+ base::Unretained(storage_.get()), conversion_id));
+}
+
+void ConversionManagerImpl::OnReportSentFromWebUI(
+ base::OnceClosure reports_sent_barrier,
+ int64_t conversion_id) {
+ // |reports_sent_barrier| is a OnceClosure view of a RepeatingClosure obtained
+ // by base::BarrierClosure().
+ storage_task_runner_->PostTaskAndReply(
+ FROM_HERE,
+ base::BindOnce(base::IgnoreResult(&ConversionStorage::DeleteConversion),
+ base::Unretained(storage_.get()), conversion_id),
+ std::move(reports_sent_barrier));
+}
+
+} // namespace content
diff --git a/chromium/content/browser/conversions/conversion_manager_impl.h b/chromium/content/browser/conversions/conversion_manager_impl.h
new file mode 100644
index 00000000000..03bcd94688f
--- /dev/null
+++ b/chromium/content/browser/conversions/conversion_manager_impl.h
@@ -0,0 +1,182 @@
+// 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_MANAGER_IMPL_H_
+#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_MANAGER_IMPL_H_
+
+#include <memory>
+#include <vector>
+
+#include "base/files/file_path.h"
+#include "base/macros.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/sequenced_task_runner.h"
+#include "base/timer/timer.h"
+#include "content/browser/conversions/conversion_manager.h"
+#include "content/browser/conversions/conversion_policy.h"
+#include "content/browser/conversions/conversion_storage.h"
+
+namespace base {
+class Clock;
+} // namespace base
+
+namespace content {
+
+// Frequency we pull ConversionReports from storage and queue them to be
+// reported.
+extern CONTENT_EXPORT const base::TimeDelta
+ kConversionManagerQueueReportsInterval;
+
+class ConversionStorage;
+class StoragePartition;
+
+// Provides access to the manager owned by the default StoragePartition.
+class ConversionManagerProviderImpl : public ConversionManager::Provider {
+ public:
+ ConversionManagerProviderImpl() = default;
+ ConversionManagerProviderImpl(const ConversionManagerProviderImpl& other) =
+ delete;
+ ConversionManagerProviderImpl& operator=(
+ const ConversionManagerProviderImpl& other) = delete;
+ ~ConversionManagerProviderImpl() override = default;
+
+ // ConversionManagerProvider:
+ ConversionManager* GetManager(WebContents* web_contents) const override;
+};
+
+// UI thread class that manages the lifetime of the underlying conversion
+// storage. Owned by the storage partition.
+class CONTENT_EXPORT ConversionManagerImpl : public ConversionManager {
+ public:
+ // Interface which manages the ownership, queuing, and sending of pending
+ // conversion reports. Owned by |this|.
+ class ConversionReporter {
+ public:
+ virtual ~ConversionReporter() = default;
+
+ // Adds |reports| to a shared queue of reports that need to be sent. Runs
+ // |report_sent_callback| for every report that is sent, with the associated
+ // |conversion_id| of the report.
+ virtual void AddReportsToQueue(
+ std::vector<ConversionReport> reports,
+ base::RepeatingCallback<void(int64_t)> report_sent_callback) = 0;
+ };
+
+ static std::unique_ptr<ConversionManagerImpl> CreateForTesting(
+ std::unique_ptr<ConversionReporter> reporter,
+ std::unique_ptr<ConversionPolicy> policy,
+ const base::Clock* clock,
+ const base::FilePath& user_data_directory,
+ scoped_refptr<base::SequencedTaskRunner> storage_task_runner);
+
+ // |storage_task_runner| should run with base::TaskPriority::BEST_EFFORT.
+ ConversionManagerImpl(
+ StoragePartition* storage_partition,
+ const base::FilePath& user_data_directory,
+ scoped_refptr<base::SequencedTaskRunner> storage_task_runner);
+ ConversionManagerImpl(const ConversionManagerImpl& other) = delete;
+ ConversionManagerImpl& operator=(const ConversionManagerImpl& other) = delete;
+ ~ConversionManagerImpl() override;
+
+ // ConversionManager:
+ void HandleImpression(const StorableImpression& impression) override;
+ void HandleConversion(const StorableConversion& conversion) override;
+ void GetActiveImpressionsForWebUI(
+ base::OnceCallback<void(std::vector<StorableImpression>)> callback)
+ override;
+ void GetReportsForWebUI(
+ base::OnceCallback<void(std::vector<ConversionReport>)> callback,
+ base::Time max_report_time) override;
+ void SendReportsForWebUI(base::OnceClosure done) override;
+ const ConversionPolicy& GetConversionPolicy() const override;
+ void ClearData(base::Time delete_begin,
+ base::Time delete_end,
+ base::RepeatingCallback<bool(const url::Origin&)> filter,
+ base::OnceClosure done) override;
+
+ private:
+ ConversionManagerImpl(
+ std::unique_ptr<ConversionReporter> reporter,
+ std::unique_ptr<ConversionPolicy> policy,
+ const base::Clock* clock,
+ const base::FilePath& user_data_directory,
+ scoped_refptr<base::SequencedTaskRunner> storage_task_runner);
+
+ void OnInitCompleted(bool success);
+
+ // Retrieves reports from storage whose |report_time| <= |max_report_time|,
+ // and calls |handler_function| on them.
+ using ReportsHandlerFunc =
+ base::OnceCallback<void(std::vector<ConversionReport>)>;
+ void GetAndHandleReports(ReportsHandlerFunc handler_function,
+ base::Time max_report_time);
+
+ // Get the next set of reports from storage that need to be sent before the
+ // next call from |get_and_queue_reports_timer_|. Adds the reports to
+ // |reporter|.
+ void GetAndQueueReportsForNextInterval();
+
+ // Queue the given |reports| on |reporter_|.
+ void QueueReports(std::vector<ConversionReport> reports);
+
+ void HandleReportsExpiredAtStartup(std::vector<ConversionReport> reports);
+
+ void HandleReportsSentFromWebUI(base::OnceClosure done,
+ std::vector<ConversionReport> reports);
+
+ // Notify storage to delete the given |conversion_id| when it's associated
+ // report has been sent.
+ void OnReportSent(int64_t conversion_id);
+
+ // Similar to OnReportSent, but invokes |reports_sent_barrier| when the
+ // report has been removed from storage.
+ void OnReportSentFromWebUI(base::OnceClosure reports_sent_barrier,
+ int64_t conversion_id);
+
+ // Friend to expose the ConversionStorage and task runner, consider changing
+ // to just expose the storage if it moves to SequenceBound.
+ friend std::vector<ConversionReport> GetConversionsToReportForTesting(
+ ConversionManagerImpl* manager,
+ base::Time max_report_time);
+
+ // Whether the API is running in debug mode, meaning that there should be
+ // no delays or noise added to reports. This is used by end to end tests to
+ // verify functionality without mocking out any implementations.
+ const bool debug_mode_;
+
+ // Task runner used to perform operations on |storage_|. Runs with
+ // base::TaskPriority::BEST_EFFORT.
+ scoped_refptr<base::SequencedTaskRunner> storage_task_runner_;
+
+ const base::Clock* clock_;
+
+ // Timer which administers calls to GetAndQueueReportsForNextInterval().
+ base::RepeatingTimer get_and_queue_reports_timer_;
+
+ // Handle keeping track of conversion reports to send. Reports are fetched
+ // from |storage_| and added to |reporter_| by |get_reports_timer_|.
+ std::unique_ptr<ConversionReporter> reporter_;
+
+ // ConversionStorage instance which is scoped to lifetime of
+ // |storage_task_runner_|. |storage_| should be accessed by calling
+ // base::PostTask with |storage_task_runner_|, and should not be accessed
+ // directly. |storage_| can be null if the storage initialization did not
+ // succeed.
+ //
+ // TODO(https://crbug.com/1066920): This should use base::SequenceBound to
+ // avoid having to call PostTask manually, as well as use base::Unretained on
+ // |storage_|.
+ std::unique_ptr<ConversionStorage, base::OnTaskRunnerDeleter> storage_;
+
+ // Policy used for controlling API configurations such as reporting and
+ // attribution models. Unique ptr so it can be overridden for testing.
+ std::unique_ptr<ConversionPolicy> conversion_policy_;
+
+ base::WeakPtrFactory<ConversionManagerImpl> weak_factory_;
+};
+
+} // namespace content
+
+#endif // CONTENT_BROWSER_CONVERSIONS_CONVERSION_MANAGER_IMPL_H_
diff --git a/chromium/content/browser/conversions/conversion_manager_impl_unittest.cc b/chromium/content/browser/conversions/conversion_manager_impl_unittest.cc
new file mode 100644
index 00000000000..3ca019fd0d1
--- /dev/null
+++ b/chromium/content/browser/conversions/conversion_manager_impl_unittest.cc
@@ -0,0 +1,365 @@
+// 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_manager_impl.h"
+
+#include <stdint.h>
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/callback_forward.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/run_loop.h"
+#include "base/sequenced_task_runner.h"
+#include "base/test/bind_test_util.h"
+#include "base/time/clock.h"
+#include "base/time/time.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 "content/public/test/browser_task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace content {
+
+namespace {
+
+constexpr base::TimeDelta kExpiredReportOffset =
+ base::TimeDelta::FromMinutes(2);
+
+class ConstantStartupDelayPolicy : public ConversionPolicy {
+ public:
+ ConstantStartupDelayPolicy() = default;
+ ~ConstantStartupDelayPolicy() override = default;
+
+ base::Time GetReportTimeForExpiredReportAtStartup(
+ base::Time now) const override {
+ return now + kExpiredReportOffset;
+ }
+};
+
+// Mock reporter that tracks reports being queued by the ConversionManager.
+class TestConversionReporter
+ : public ConversionManagerImpl::ConversionReporter {
+ public:
+ TestConversionReporter() = default;
+ ~TestConversionReporter() override = default;
+
+ // ConversionManagerImpl::ConversionReporter
+ void AddReportsToQueue(
+ std::vector<ConversionReport> reports,
+ base::RepeatingCallback<void(int64_t)> report_sent_callback) override {
+ num_reports_ += reports.size();
+ last_conversion_id_ = *reports.back().conversion_id;
+ last_report_time_ = reports.back().report_time;
+
+ if (should_run_report_sent_callbacks_) {
+ for (const auto& report : reports) {
+ report_sent_callback.Run(*report.conversion_id);
+ }
+ }
+
+ if (quit_closure_ && num_reports_ >= expected_num_reports_)
+ std::move(quit_closure_).Run();
+ }
+
+ void ShouldRunReportSentCallbacks(bool should_run_report_sent_callbacks) {
+ should_run_report_sent_callbacks_ = should_run_report_sent_callbacks;
+ }
+
+ size_t num_reports() { return num_reports_; }
+
+ int64_t last_conversion_id() { return last_conversion_id_; }
+
+ base::Time last_report_time() { return last_report_time_; }
+
+ void WaitForNumReports(size_t expected_num_reports) {
+ if (num_reports_ >= expected_num_reports)
+ return;
+
+ expected_num_reports_ = expected_num_reports;
+ base::RunLoop wait_loop;
+ quit_closure_ = wait_loop.QuitClosure();
+ wait_loop.Run();
+ }
+
+ private:
+ bool should_run_report_sent_callbacks_ = false;
+ size_t expected_num_reports_ = 0u;
+ size_t num_reports_ = 0u;
+ int64_t last_conversion_id_ = 0UL;
+ base::Time last_report_time_;
+ base::OnceClosure quit_closure_;
+};
+
+// Time after impression that a conversion can first be sent. See
+// ConversionStorageDelegateImpl::GetReportTimeForConversion().
+constexpr base::TimeDelta kFirstReportingWindow = base::TimeDelta::FromDays(2);
+
+// Give impressions a sufficiently long expiry.
+constexpr base::TimeDelta kImpressionExpiry = base::TimeDelta::FromDays(30);
+
+} // namespace
+
+class ConversionManagerImplTest : public testing::Test {
+ public:
+ ConversionManagerImplTest()
+ : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
+ EXPECT_TRUE(dir_.CreateUniqueTempDir());
+ CreateManager();
+ }
+
+ void CreateManager() {
+ auto reporter = std::make_unique<TestConversionReporter>();
+ test_reporter_ = reporter.get();
+ conversion_manager_ = ConversionManagerImpl::CreateForTesting(
+ std::move(reporter), std::make_unique<ConstantStartupDelayPolicy>(),
+ task_environment_.GetMockClock(), dir_.GetPath(),
+ base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()}));
+ }
+
+ const base::Clock& clock() { return *task_environment_.GetMockClock(); }
+
+ protected:
+ base::ScopedTempDir dir_;
+ BrowserTaskEnvironment task_environment_;
+ std::unique_ptr<ConversionManagerImpl> conversion_manager_;
+ TestConversionReporter* test_reporter_ = nullptr;
+};
+
+TEST_F(ConversionManagerImplTest, ImpressionRegistered_ReturnedToWebUI) {
+ auto impression = ImpressionBuilder(clock().Now())
+ .SetExpiry(kImpressionExpiry)
+ .SetData("100")
+ .Build();
+ conversion_manager_->HandleImpression(impression);
+
+ base::RunLoop run_loop;
+ auto get_impressions_callback = base::BindLambdaForTesting(
+ [&](std::vector<StorableImpression> impressions) {
+ EXPECT_EQ(1u, impressions.size());
+ EXPECT_TRUE(ImpressionsEqual(impression, impressions.back()));
+ run_loop.Quit();
+ });
+ conversion_manager_->GetActiveImpressionsForWebUI(
+ std::move(get_impressions_callback));
+ run_loop.Run();
+}
+
+TEST_F(ConversionManagerImplTest, ExpiredImpression_NotReturnedToWebUI) {
+ conversion_manager_->HandleImpression(ImpressionBuilder(clock().Now())
+ .SetExpiry(kImpressionExpiry)
+ .SetData("100")
+ .Build());
+ task_environment_.FastForwardBy(2 * kImpressionExpiry);
+
+ base::RunLoop run_loop;
+ auto get_impressions_callback = base::BindLambdaForTesting(
+ [&](std::vector<StorableImpression> impressions) {
+ EXPECT_TRUE(impressions.empty());
+ run_loop.Quit();
+ });
+ conversion_manager_->GetActiveImpressionsForWebUI(
+ std::move(get_impressions_callback));
+ run_loop.Run();
+}
+
+TEST_F(ConversionManagerImplTest, ImpressionConverted_ReportReturnedToWebUI) {
+ auto impression = ImpressionBuilder(clock().Now())
+ .SetExpiry(kImpressionExpiry)
+ .SetData("100")
+ .Build();
+ conversion_manager_->HandleImpression(impression);
+
+ auto conversion = DefaultConversion();
+ conversion_manager_->HandleConversion(conversion);
+
+ ConversionReport expected_report(impression, conversion.conversion_data(),
+ clock().Now() + kFirstReportingWindow,
+ base::nullopt /* conversion_id */);
+ expected_report.attribution_credit = 100;
+
+ base::RunLoop run_loop;
+ auto reports_callback =
+ base::BindLambdaForTesting([&](std::vector<ConversionReport> reports) {
+ EXPECT_EQ(1u, reports.size());
+ EXPECT_TRUE(ReportsEqual({expected_report}, reports));
+ run_loop.Quit();
+ });
+ conversion_manager_->GetReportsForWebUI(std::move(reports_callback),
+ base::Time::Max());
+ run_loop.Run();
+}
+
+TEST_F(ConversionManagerImplTest, ImpressionConverted_ReportQueued) {
+ conversion_manager_->HandleImpression(
+ ImpressionBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
+ conversion_manager_->HandleConversion(DefaultConversion());
+
+ // Reports are queued in intervals ahead of when they should be
+ // sent. Make sure the report is not queued earlier than this.
+ task_environment_.FastForwardBy(kFirstReportingWindow -
+ kConversionManagerQueueReportsInterval -
+ base::TimeDelta::FromMinutes(1));
+ EXPECT_EQ(0u, test_reporter_->num_reports());
+
+ task_environment_.FastForwardBy(base::TimeDelta::FromMinutes(1));
+ EXPECT_EQ(1u, test_reporter_->num_reports());
+}
+
+TEST_F(ConversionManagerImplTest, QueuedReportNotSent_QueuedAgain) {
+ conversion_manager_->HandleImpression(
+ ImpressionBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
+ conversion_manager_->HandleConversion(DefaultConversion());
+ task_environment_.FastForwardBy(kFirstReportingWindow -
+ kConversionManagerQueueReportsInterval);
+ EXPECT_EQ(1u, test_reporter_->num_reports());
+
+ // If the report is not sent, it should be added to the queue again.
+ task_environment_.FastForwardBy(kConversionManagerQueueReportsInterval);
+ EXPECT_EQ(2u, test_reporter_->num_reports());
+}
+
+TEST_F(ConversionManagerImplTest, QueuedReportSent_NotQueuedAgain) {
+ test_reporter_->ShouldRunReportSentCallbacks(true);
+ conversion_manager_->HandleImpression(
+ ImpressionBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
+ conversion_manager_->HandleConversion(DefaultConversion());
+ task_environment_.FastForwardBy(kFirstReportingWindow -
+ kConversionManagerQueueReportsInterval);
+ EXPECT_EQ(1u, test_reporter_->num_reports());
+
+ // The report should not be added to the queue again.
+ task_environment_.FastForwardBy(kConversionManagerQueueReportsInterval);
+ EXPECT_EQ(1u, test_reporter_->num_reports());
+}
+
+// Add a conversion to storage and reset the manager to mimic a report being
+// available at startup.
+TEST_F(ConversionManagerImplTest, ExpiredReportsAtStartup_Queued) {
+ // Create a report that will be reported at t= 2 days.
+ conversion_manager_->HandleImpression(
+ ImpressionBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
+ conversion_manager_->HandleConversion(DefaultConversion());
+
+ // Create another conversion that will be reported at t=
+ // (kFirstReportingWindow + 2 * kConversionManagerQueueReportsInterval).
+ task_environment_.FastForwardBy(2 * kConversionManagerQueueReportsInterval);
+ conversion_manager_->HandleImpression(
+ ImpressionBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
+ conversion_manager_->HandleConversion(DefaultConversion());
+
+ EXPECT_EQ(0u, test_reporter_->num_reports());
+
+ // Reset the manager to simulate shutdown.
+ conversion_manager_.reset();
+
+ // Fast forward past the expected report time of the first conversion, t =
+ // (kFirstReportingWindow+ 1 minute).
+ task_environment_.FastForwardBy(kFirstReportingWindow -
+ (2 * kConversionManagerQueueReportsInterval) +
+ base::TimeDelta::FromMinutes(1));
+
+ // Create the manager and check that the first report is queued immediately.
+ CreateManager();
+ test_reporter_->ShouldRunReportSentCallbacks(true);
+ test_reporter_->WaitForNumReports(1);
+ EXPECT_EQ(1u, test_reporter_->num_reports());
+
+ // The second report is still queued at the correct time.
+ task_environment_.FastForwardBy(kConversionManagerQueueReportsInterval);
+ EXPECT_EQ(2u, test_reporter_->num_reports());
+}
+
+// This functionality is tested more thoroughly in the ConversionStorageSql
+// unit tests. Here, just test to make sure the basic control flow is working.
+TEST_F(ConversionManagerImplTest, ClearData) {
+ for (bool match_url : {true, false}) {
+ base::Time start = clock().Now();
+ conversion_manager_->HandleImpression(
+ ImpressionBuilder(start).SetExpiry(kImpressionExpiry).Build());
+ conversion_manager_->HandleConversion(DefaultConversion());
+
+ base::RunLoop run_loop;
+ conversion_manager_->ClearData(
+ start, start + base::TimeDelta::FromMinutes(1),
+ base::BindLambdaForTesting(
+ [match_url](const url::Origin& _) { return match_url; }),
+ run_loop.QuitClosure());
+ run_loop.Run();
+
+ task_environment_.FastForwardBy(kFirstReportingWindow -
+ kConversionManagerQueueReportsInterval);
+ size_t expected_reports = match_url ? 0u : 1u;
+ EXPECT_EQ(expected_reports, test_reporter_->num_reports());
+ }
+}
+
+TEST_F(ConversionManagerImplTest, ConversionsSentFromUI_ReportedImmediately) {
+ conversion_manager_->HandleImpression(
+ ImpressionBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
+ conversion_manager_->HandleImpression(
+ ImpressionBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
+ conversion_manager_->HandleConversion(DefaultConversion());
+ EXPECT_EQ(0u, test_reporter_->num_reports());
+
+ conversion_manager_->SendReportsForWebUI(base::DoNothing());
+ task_environment_.FastForwardBy(base::TimeDelta::FromMinutes(0));
+ EXPECT_EQ(2u, test_reporter_->num_reports());
+}
+
+TEST_F(ConversionManagerImplTest, ExpiredReportsAtStartup_Delayed) {
+ // Create a report that will be reported at t= 2 days.
+ base::Time start_time = clock().Now();
+ conversion_manager_->HandleImpression(
+ ImpressionBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
+ conversion_manager_->HandleConversion(DefaultConversion());
+ EXPECT_EQ(0u, test_reporter_->num_reports());
+
+ // Reset the manager to simulate shutdown.
+ conversion_manager_.reset();
+
+ // Fast forward past the expected report time of the first conversion, t =
+ // (kFirstReportingWindow+ 1 minute).
+ task_environment_.FastForwardBy(kFirstReportingWindow +
+ base::TimeDelta::FromMinutes(1));
+
+ CreateManager();
+ test_reporter_->WaitForNumReports(1);
+
+ // Ensure that the expired report is delayed based on the time the browser
+ // started.
+ EXPECT_EQ(start_time + kFirstReportingWindow +
+ base::TimeDelta::FromMinutes(1) + kExpiredReportOffset,
+ test_reporter_->last_report_time());
+}
+
+TEST_F(ConversionManagerImplTest, NonExpiredReportsQueuedAtStartup_NotDelayed) {
+ // Create a report that will be reported at t= 2 days.
+ base::Time start_time = clock().Now();
+ conversion_manager_->HandleImpression(
+ ImpressionBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
+ conversion_manager_->HandleConversion(DefaultConversion());
+ EXPECT_EQ(0u, test_reporter_->num_reports());
+
+ // Reset the manager to simulate shutdown.
+ conversion_manager_.reset();
+
+ // Fast forward just before the expected report time.
+ task_environment_.FastForwardBy(kFirstReportingWindow -
+ base::TimeDelta::FromMinutes(1));
+
+ // Ensure that this report does not receive additional delay.
+ CreateManager();
+ test_reporter_->WaitForNumReports(1);
+ EXPECT_EQ(1u, test_reporter_->num_reports());
+ EXPECT_EQ(start_time + kFirstReportingWindow,
+ test_reporter_->last_report_time());
+}
+
+} // namespace content
diff --git a/chromium/content/browser/conversions/conversion_network_sender_impl.cc b/chromium/content/browser/conversions/conversion_network_sender_impl.cc
new file mode 100644
index 00000000000..d2259fbf15b
--- /dev/null
+++ b/chromium/content/browser/conversions/conversion_network_sender_impl.cc
@@ -0,0 +1,177 @@
+// 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_network_sender_impl.h"
+
+#include <string>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/check.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/strings/strcat.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/time/time.h"
+#include "content/public/browser/storage_partition.h"
+#include "net/base/load_flags.h"
+#include "net/base/net_errors.h"
+#include "net/http/http_request_headers.h"
+#include "net/http/http_response_headers.h"
+#include "net/http/http_status_code.h"
+#include "net/traffic_annotation/network_traffic_annotation.h"
+#include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/cpp/simple_url_loader.h"
+#include "url/gurl.h"
+#include "url/origin.h"
+#include "url/url_canon.h"
+
+namespace content {
+
+namespace {
+
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused.
+enum class Status {
+ kOk = 0,
+ // Corresponds to a non-zero NET_ERROR.
+ kInternalError = 1,
+ // Corresponds to a non-200 HTTP response code from the reporting endpoint.
+ kExternalError = 2,
+ kMaxValue = kExternalError
+};
+
+// Called when a network request is started for |report|, for logging metrics.
+void LogMetricsOnReportSend(ConversionReport* report) {
+ DCHECK(report);
+
+ // Reports sent from the WebUI should not log metrics.
+ if (report->report_time == base::Time::Min())
+ return;
+
+ // Use a large time range to capture users that might not open the browser for
+ // a long time while a conversion report is pending. Revisit this range if it
+ // is non-ideal for real world data.
+ // Add |extra_delay| to the reported time which will include the amount of
+ // time since the report was originally scheduled, for reports at startup
+ // whose |report_time| changes due to additional startup delay.
+ base::Time now = base::Time::Now();
+ base::TimeDelta delay = (now - report->report_time) + report->extra_delay;
+ base::UmaHistogramCustomTimes("Conversions.ExtraReportDelay", delay,
+ base::TimeDelta::FromSeconds(1),
+ base::TimeDelta::FromDays(7), /*buckets=*/100);
+
+ // TODO(csharrison): We should thread the conversion time alongside the
+ // ConversionReport to log the effective time since conversion.
+}
+
+GURL GetReportUrl(const content::ConversionReport& report) {
+ url::Replacements<char> replacements;
+ const char kEndpointPath[] = "/.well-known/register-conversion";
+ replacements.SetPath(kEndpointPath, url::Component(0, strlen(kEndpointPath)));
+ std::string query = base::StrCat(
+ {"impression-data=", report.impression.impression_data(),
+ "&conversion-data=", report.conversion_data,
+ "&credit=", base::NumberToString(report.attribution_credit)});
+ replacements.SetQuery(query.c_str(), url::Component(0, query.length()));
+ return report.impression.reporting_origin().GetURL().ReplaceComponents(
+ replacements);
+}
+
+} // namespace
+
+ConversionNetworkSenderImpl::ConversionNetworkSenderImpl(
+ StoragePartition* storage_partition)
+ : storage_partition_(storage_partition) {}
+
+ConversionNetworkSenderImpl::~ConversionNetworkSenderImpl() = default;
+
+void ConversionNetworkSenderImpl::SendReport(ConversionReport* report,
+ ReportSentCallback sent_callback) {
+ // The browser process URLLoaderFactory is not created by default, so don't
+ // create it until it is directly needed.
+ if (!url_loader_factory_) {
+ url_loader_factory_ =
+ storage_partition_->GetURLLoaderFactoryForBrowserProcess();
+ }
+
+ auto resource_request = std::make_unique<network::ResourceRequest>();
+ resource_request->url = GetReportUrl(*report);
+ resource_request->referrer = report->impression.conversion_origin().GetURL();
+ resource_request->method = net::HttpRequestHeaders::kPostMethod;
+ resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
+ resource_request->load_flags =
+ net::LOAD_DISABLE_CACHE | net::LOAD_BYPASS_CACHE;
+
+ // TODO(https://crbug.com/1058018): Update the "policy" field in the traffic
+ // annotation when a setting to disable the API is properly
+ // surfaced/implemented.
+ net::NetworkTrafficAnnotationTag traffic_annotation =
+ net::DefineNetworkTrafficAnnotation("conversion_measurement_report", R"(
+ semantics {
+ sender: "Event-level Conversion Measurement API"
+ description:
+ "The Conversion Measurement API allows sites to measure "
+ "conversions (e.g. purchases) and attribute them to clicked ads, "
+ "without using cross-site persistent identifiers like third party "
+ "cookies."
+ trigger:
+ "When a registered conversion has become eligible for reporting."
+ data:
+ "A high-entropy identifier declared by the site in which the user "
+ "clicked on an impression. A noisy low entropy data value declared "
+ "on the conversion site. A browser generated value that denotes "
+ "if this was the last impression clicked prior to conversion."
+ destination:OTHER
+ }
+ policy {
+ cookies_allowed: NO
+ setting:
+ "This feature cannot be disabled by settings."
+ policy_exception_justification: "Not implemented."
+ })");
+
+ auto simple_url_loader = network::SimpleURLLoader::Create(
+ std::move(resource_request), traffic_annotation);
+ network::SimpleURLLoader* simple_url_loader_ptr = simple_url_loader.get();
+
+ auto it = loaders_in_progress_.insert(loaders_in_progress_.begin(),
+ std::move(simple_url_loader));
+ simple_url_loader_ptr->SetTimeoutDuration(base::TimeDelta::FromSeconds(30));
+
+ // Unretained is safe because the URLLoader is owned by |this| and will be
+ // deleted before |this|.
+ simple_url_loader_ptr->DownloadHeadersOnly(
+ url_loader_factory_.get(),
+ base::BindOnce(&ConversionNetworkSenderImpl::OnReportSent,
+ base::Unretained(this), std::move(it),
+ std::move(sent_callback)));
+ LogMetricsOnReportSend(report);
+}
+
+void ConversionNetworkSenderImpl::SetURLLoaderFactoryForTesting(
+ scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) {
+ url_loader_factory_ = url_loader_factory;
+}
+
+void ConversionNetworkSenderImpl::OnReportSent(
+ UrlLoaderList::iterator it,
+ ReportSentCallback sent_callback,
+ scoped_refptr<net::HttpResponseHeaders> headers) {
+ network::SimpleURLLoader* loader = it->get();
+
+ // Consider a non-200 HTTP code as a non-internal error.
+ bool internal_ok = loader->NetError() == net::OK ||
+ loader->NetError() == net::ERR_HTTP_RESPONSE_CODE_FAILURE;
+ bool external_ok = headers && headers->response_code() == net::HTTP_OK;
+ Status status =
+ internal_ok && external_ok
+ ? Status::kOk
+ : !internal_ok ? Status::kInternalError : Status::kExternalError;
+ base::UmaHistogramEnumeration("Conversions.ReportStatus", status);
+
+ loaders_in_progress_.erase(it);
+ std::move(sent_callback).Run();
+}
+
+} // namespace content
diff --git a/chromium/content/browser/conversions/conversion_network_sender_impl.h b/chromium/content/browser/conversions/conversion_network_sender_impl.h
new file mode 100644
index 00000000000..0dd23717171
--- /dev/null
+++ b/chromium/content/browser/conversions/conversion_network_sender_impl.h
@@ -0,0 +1,71 @@
+// 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_NETWORK_SENDER_IMPL_H_
+#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_NETWORK_SENDER_IMPL_H_
+
+#include <stdint.h>
+#include <list>
+#include <memory>
+
+#include "base/callback.h"
+#include "content/browser/conversions/conversion_report.h"
+#include "content/browser/conversions/conversion_reporter_impl.h"
+#include "content/common/content_export.h"
+#include "services/network/public/cpp/shared_url_loader_factory.h"
+
+namespace network {
+class SimpleURLLoader;
+} // namespace network
+
+namespace content {
+
+class StoragePartition;
+
+// Implemented a NetworkSender capable of issuing POST requests for complete
+// conversions. Maintains a set of all ongoing UrlLoaders used for posting
+// conversion reports. Created and owned by ConversionReporterImpl.
+class CONTENT_EXPORT ConversionNetworkSenderImpl
+ : public ConversionReporterImpl::NetworkSender {
+ public:
+ explicit ConversionNetworkSenderImpl(StoragePartition* storage_partition);
+ ConversionNetworkSenderImpl(const ConversionNetworkSenderImpl&) = delete;
+ ConversionNetworkSenderImpl& operator=(const ConversionNetworkSenderImpl&) =
+ delete;
+ ~ConversionNetworkSenderImpl() override;
+
+ // Generates a resource request for |report| and creates a new UrlLoader to
+ // send it. A report is only attempted to be sent once, with a timeout of 30
+ // seconds. |report| is destroyed after this call finishes.
+ // |sent_callback| is run after the request finishes, whether or not it
+ // succeeded,
+ void SendReport(ConversionReport* report,
+ ReportSentCallback sent_callback) override;
+
+ // Tests inject a TestURLLoaderFactory so they can mock the network response.
+ void SetURLLoaderFactoryForTesting(
+ scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory);
+
+ private:
+ // This is a std::list so that iterators remain valid during modifications.
+ using UrlLoaderList = std::list<std::unique_ptr<network::SimpleURLLoader>>;
+
+ // Called when headers are available for a sent report.
+ void OnReportSent(UrlLoaderList::iterator it,
+ ReportSentCallback sent_callback,
+ scoped_refptr<net::HttpResponseHeaders> headers);
+
+ // Reports that are actively being sent.
+ UrlLoaderList loaders_in_progress_;
+
+ // Must outlive |this|.
+ StoragePartition* storage_partition_;
+
+ // Lazily accessed URLLoaderFactory used for network requests.
+ scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
+};
+
+} // namespace content
+
+#endif // CONTENT_BROWSER_CONVERSIONS_CONVERSION_NETWORK_SENDER_IMPL_H_
diff --git a/chromium/content/browser/conversions/conversion_network_sender_impl_unittest.cc b/chromium/content/browser/conversions/conversion_network_sender_impl_unittest.cc
new file mode 100644
index 00000000000..87e0bfc87c1
--- /dev/null
+++ b/chromium/content/browser/conversions/conversion_network_sender_impl_unittest.cc
@@ -0,0 +1,252 @@
+// 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_network_sender_impl.h"
+
+#include <string>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/strings/strcat.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/task/post_task.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/test/simple_test_clock.h"
+#include "content/browser/conversions/conversion_test_utils.h"
+#include "content/browser/storage_partition_impl.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/storage_partition.h"
+#include "content/public/common/content_features.h"
+#include "content/public/test/browser_task_environment.h"
+#include "content/public/test/test_browser_context.h"
+#include "net/base/load_flags.h"
+#include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/cpp/shared_url_loader_factory.h"
+#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
+#include "services/network/test/test_url_loader_factory.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace content {
+
+namespace {
+
+// Gets a report url which matches reports created by GetReport().
+std::string GetReportUrl(std::string impression_data) {
+ return base::StrCat(
+ {"https://report.test/.well-known/register-conversion?impression-data=",
+ impression_data, "&conversion-data=", impression_data, "&credit=0"});
+}
+
+// Create a simple report where impression data/conversion data/conversion id
+// are all the same.
+ConversionReport GetReport(int64_t conversion_id) {
+ return ConversionReport(
+ ImpressionBuilder(base::Time())
+ .SetData(base::NumberToString(conversion_id))
+ .Build(),
+ /*conversion_data=*/base::NumberToString(conversion_id),
+ /*report_time=*/base::Time(),
+ /*conversion_id=*/conversion_id);
+}
+
+} // namespace
+
+class ConversionNetworkSenderTest : public testing::Test {
+ public:
+ ConversionNetworkSenderTest()
+ : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME),
+ network_sender_(std::make_unique<ConversionNetworkSenderImpl>(
+ /*sotrage_partition=*/nullptr)),
+ shared_url_loader_factory_(
+ base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
+ &test_url_loader_factory_)) {
+ network_sender_->SetURLLoaderFactoryForTesting(shared_url_loader_factory_);
+ }
+
+ ConversionReporterImpl::NetworkSender::ReportSentCallback GetSentCallback() {
+ return base::BindOnce(&ConversionNetworkSenderTest::OnReportSent,
+ base::Unretained(this));
+ }
+
+ protected:
+ size_t num_reports_sent_ = 0u;
+
+ // |task_enviorment_| must be initialized first.
+ content::BrowserTaskEnvironment task_environment_;
+
+ // Unique ptr so it can be reset during testing.
+ std::unique_ptr<ConversionNetworkSenderImpl> network_sender_;
+ network::TestURLLoaderFactory test_url_loader_factory_;
+
+ private:
+ void OnReportSent() { num_reports_sent_++; }
+
+ scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory_;
+};
+
+TEST_F(ConversionNetworkSenderTest,
+ ConversionReportReceived_NetworkRequestMade) {
+ auto report = GetReport(/*conversion_id=*/1);
+ network_sender_->SendReport(&report, std::move(base::DoNothing()));
+ EXPECT_EQ(1, test_url_loader_factory_.NumPending());
+ EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest(
+ GetReportUrl("1"), ""));
+}
+
+TEST_F(ConversionNetworkSenderTest, ReportSent_CallbackFired) {
+ auto report = GetReport(/*conversion_id=*/1);
+ network_sender_->SendReport(&report, GetSentCallback());
+ EXPECT_EQ(1, test_url_loader_factory_.NumPending());
+ EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest(
+ GetReportUrl("1"), ""));
+ EXPECT_EQ(1u, num_reports_sent_);
+}
+
+TEST_F(ConversionNetworkSenderTest, SenderDeletedDuringRequest_NoCrash) {
+ auto report = GetReport(/*conversion_id=*/1);
+ network_sender_->SendReport(&report, GetSentCallback());
+ EXPECT_EQ(1, test_url_loader_factory_.NumPending());
+ network_sender_.reset();
+ EXPECT_FALSE(test_url_loader_factory_.SimulateResponseForPendingRequest(
+ GetReportUrl("1"), ""));
+ EXPECT_EQ(0u, num_reports_sent_);
+}
+
+TEST_F(ConversionNetworkSenderTest, ReportRequestHangs_TimesOut) {
+ auto report = GetReport(/*conversion_id=*/1);
+ network_sender_->SendReport(&report, GetSentCallback());
+ EXPECT_EQ(1, test_url_loader_factory_.NumPending());
+
+ // The request should time out after 30 seconds.
+ task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(30));
+
+ EXPECT_EQ(0, test_url_loader_factory_.NumPending());
+
+ // Also verify that the sent callback runs if the request times out.
+ EXPECT_EQ(1u, num_reports_sent_);
+}
+
+TEST_F(ConversionNetworkSenderTest, ReportSent_QueryParamsSetCorrectly) {
+ auto impression =
+ ImpressionBuilder(base::Time())
+ .SetData("impression")
+ .SetReportingOrigin(url::Origin::Create(GURL("https://a.com")))
+ .Build();
+ ConversionReport report(impression,
+ /*conversion_data=*/"conversion",
+ /*report_time=*/base::Time(),
+ /*conversion_id=*/1);
+ report.attribution_credit = 50;
+ network_sender_->SendReport(&report, base::DoNothing());
+
+ std::string expected_report_url(
+ "https://a.com/.well-known/"
+ "register-conversion?impression-data=impression&conversion-data="
+ "conversion&credit=50");
+ EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest(
+ expected_report_url, ""));
+}
+
+TEST_F(ConversionNetworkSenderTest, ReportSent_RequestAttributesSet) {
+ auto impression =
+ ImpressionBuilder(base::Time())
+ .SetData("1")
+ .SetReportingOrigin(url::Origin::Create(GURL("https://a.com")))
+ .SetConversionOrigin(url::Origin::Create(GURL("https://b.com")))
+ .Build();
+ ConversionReport report(impression,
+ /*conversion_data=*/"1",
+ /*report_time=*/base::Time(),
+ /*conversion_id=*/1);
+ network_sender_->SendReport(&report, base::DoNothing());
+
+ const network::ResourceRequest* pending_request;
+ std::string expected_report_url(
+ "https://a.com/.well-known/"
+ "register-conversion?impression-data=1&conversion-data=1&credit=0");
+ EXPECT_TRUE(test_url_loader_factory_.IsPending(expected_report_url,
+ &pending_request));
+
+ // Ensure that the request is sent with no credentials.
+ EXPECT_EQ(network::mojom::CredentialsMode::kOmit,
+ pending_request->credentials_mode);
+ EXPECT_EQ("POST", pending_request->method);
+ EXPECT_EQ(GURL("https://b.com"), pending_request->referrer);
+}
+
+TEST_F(ConversionNetworkSenderTest, ReportResultsInHttpError_SentCallbackRuns) {
+ auto report = GetReport(/*conversion_id=*/1);
+ network_sender_->SendReport(&report, GetSentCallback());
+ EXPECT_EQ(0u, num_reports_sent_);
+
+ // We should run the sent callback even if there is an http error.
+ EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest(
+ GetReportUrl("1"), "", net::HttpStatusCode::HTTP_BAD_REQUEST));
+ EXPECT_EQ(1u, num_reports_sent_);
+}
+
+TEST_F(ConversionNetworkSenderTest, ManyReports_AllSentSuccessfully) {
+ for (int i = 0; i < 10; i++) {
+ auto report = GetReport(/*conversion_id=*/i);
+ network_sender_->SendReport(&report, GetSentCallback());
+ }
+ EXPECT_EQ(10, test_url_loader_factory_.NumPending());
+
+ // Send reports out of order to guarantee that callback conversion_ids are
+ // properly handled.
+ for (int i = 9; i >= 0; i--) {
+ std::string report_id = base::NumberToString(i);
+
+ EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest(
+ GetReportUrl(report_id), ""));
+ }
+ EXPECT_EQ(10u, num_reports_sent_);
+ EXPECT_EQ(0, test_url_loader_factory_.NumPending());
+}
+
+TEST_F(ConversionNetworkSenderTest, LoadFlags) {
+ auto report = GetReport(/*conversion_id=*/1);
+ network_sender_->SendReport(&report, GetSentCallback());
+ int load_flags =
+ test_url_loader_factory_.GetPendingRequest(0)->request.load_flags;
+ EXPECT_TRUE(load_flags & net::LOAD_BYPASS_CACHE);
+ EXPECT_TRUE(load_flags & net::LOAD_DISABLE_CACHE);
+}
+
+TEST_F(ConversionNetworkSenderTest, ErrorHistogram) {
+ // All OK.
+ {
+ base::HistogramTester histograms;
+ auto report = GetReport(/*conversion_id=*/1);
+ network_sender_->SendReport(&report, GetSentCallback());
+ EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest(
+ GetReportUrl("1"), ""));
+ // kOk = 0.
+ histograms.ExpectUniqueSample("Conversions.ReportStatus", 0, 1);
+ }
+ // Internal error.
+ {
+ base::HistogramTester histograms;
+ auto report = GetReport(/*conversion_id=*/2);
+ network_sender_->SendReport(&report, GetSentCallback());
+ network::URLLoaderCompletionStatus completion_status(net::ERR_FAILED);
+ EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest(
+ GURL(GetReportUrl("2")), completion_status,
+ network::mojom::URLResponseHead::New(), std::string()));
+ // kInternalError = 1.
+ histograms.ExpectUniqueSample("Conversions.ReportStatus", 1, 1);
+ }
+ {
+ base::HistogramTester histograms;
+ auto report = GetReport(/*conversion_id=*/3);
+ network_sender_->SendReport(&report, GetSentCallback());
+ EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest(
+ GetReportUrl("3"), std::string(), net::HTTP_UNAUTHORIZED));
+ // kExternalError = 2.
+ histograms.ExpectUniqueSample("Conversions.ReportStatus", 2, 1);
+ }
+}
+
+} // namespace content
diff --git a/chromium/content/browser/conversions/conversion_page_metrics.cc b/chromium/content/browser/conversions/conversion_page_metrics.cc
new file mode 100644
index 00000000000..616ec324d67
--- /dev/null
+++ b/chromium/content/browser/conversions/conversion_page_metrics.cc
@@ -0,0 +1,24 @@
+// 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_page_metrics.h"
+
+#include "base/metrics/histogram_functions.h"
+
+namespace content {
+
+ConversionPageMetrics::ConversionPageMetrics() = default;
+
+ConversionPageMetrics::~ConversionPageMetrics() {
+ // TODO(https://crbug.com/1044099): Consider limiting registrations per page
+ // based on this metric.
+ base::UmaHistogramExactLinear("Conversions.RegisteredConversionsPerPage",
+ num_conversions_on_current_page_, 100);
+}
+
+void ConversionPageMetrics::OnConversion(const StorableConversion& conversion) {
+ num_conversions_on_current_page_++;
+}
+
+} // namespace content
diff --git a/chromium/content/browser/conversions/conversion_page_metrics.h b/chromium/content/browser/conversions/conversion_page_metrics.h
new file mode 100644
index 00000000000..bd974b1098e
--- /dev/null
+++ b/chromium/content/browser/conversions/conversion_page_metrics.h
@@ -0,0 +1,33 @@
+// 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_PAGE_METRICS_H_
+#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_PAGE_METRICS_H_
+
+namespace content {
+
+class StorableConversion;
+
+// Keeps track of per-page-load metrics for conversion measurement. Lifetime is
+// scoped to a single page load.
+class ConversionPageMetrics {
+ public:
+ ConversionPageMetrics();
+ ~ConversionPageMetrics();
+
+ ConversionPageMetrics(const ConversionPageMetrics& other) = delete;
+ ConversionPageMetrics& operator=(const ConversionPageMetrics& other) = delete;
+
+ // Called when a conversion is registered.
+ void OnConversion(const StorableConversion& conversion);
+
+ private:
+ // Keeps track of how many conversion registrations there have been on the
+ // current page.
+ int num_conversions_on_current_page_ = 0;
+};
+
+} // namespace content
+
+#endif // CONTENT_BROWSER_CONVERSIONS_CONVERSION_PAGE_METRICS_H_
diff --git a/chromium/content/browser/conversions/conversion_policy.cc b/chromium/content/browser/conversions/conversion_policy.cc
index d323ccfe3b7..4df9ed74183 100644
--- a/chromium/content/browser/conversions/conversion_policy.cc
+++ b/chromium/content/browser/conversions/conversion_policy.cc
@@ -5,7 +5,6 @@
#include "content/browser/conversions/conversion_policy.h"
#include "base/format_macros.h"
-#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/rand_util.h"
#include "base/strings/stringprintf.h"
@@ -41,97 +40,24 @@ std::unique_ptr<ConversionPolicy> ConversionPolicy::CreateForTesting(
new ConversionPolicy(std::move(noise_provider)));
}
-ConversionPolicy::ConversionPolicy()
- : noise_provider_(std::make_unique<NoiseProvider>()) {}
+ConversionPolicy::ConversionPolicy(bool debug_mode)
+ : debug_mode_(debug_mode),
+ noise_provider_(debug_mode ? nullptr
+ : std::make_unique<NoiseProvider>()) {}
ConversionPolicy::ConversionPolicy(
std::unique_ptr<ConversionPolicy::NoiseProvider> noise_provider)
- : noise_provider_(std::move(noise_provider)) {}
+ : debug_mode_(false), noise_provider_(std::move(noise_provider)) {}
ConversionPolicy::~ConversionPolicy() = default;
-base::Time ConversionPolicy::GetReportTimeForConversion(
- const ConversionReport& report) const {
- // After the initial impression, a schedule of reporting windows and deadlines
- // associated with that impression begins. The time between impression time
- // and impression expiry is split into multiple reporting windows. At the end
- // of each window, the browser will send all scheduled reports for that
- // impression.
- //
- // Each reporting window has a deadline and only conversions registered before
- // that deadline are sent in that window. Each deadline is one hour prior to
- // the window report time. The deadlines relative to impression time are <2
- // days minus 1 hour, 7 days minus 1 hour, impression expiry>. The impression
- // expiry window is only used for conversions that occur after the 7 day
- // deadline. For example, a conversion which happens one hour after an
- // impression with an expiry of two hours, is still reported in the 2 day
- // window.
- constexpr base::TimeDelta kWindowDeadlineOffset =
- base::TimeDelta::FromHours(1);
- base::TimeDelta expiry_deadline =
- report.impression.expiry_time() - report.impression.impression_time();
- const base::TimeDelta kReportingWindowDeadlines[] = {
- base::TimeDelta::FromDays(2) - kWindowDeadlineOffset,
- base::TimeDelta::FromDays(7) - kWindowDeadlineOffset, expiry_deadline};
-
- base::TimeDelta deadline_to_use;
-
- // Given a conversion report that was created at |report.report_time|, find
- // the first applicable reporting window this conversion should be reported
- // at.
- for (base::TimeDelta report_window_deadline : kReportingWindowDeadlines) {
- // If this window is valid for the conversion, use it. |report.report_time|
- // is roughly ~now, as the conversion time is used as the default value for
- // newly created reports that have not had a report time set.
- if (report.impression.impression_time() + report_window_deadline >=
- report.report_time) {
- deadline_to_use = report_window_deadline;
- break;
- }
- }
-
- // Valid conversion reports should always have a valid reporting deadline.
- DCHECK(!deadline_to_use.is_zero());
-
- // If the expiry deadline falls after the first window, but before another
- // window, use it instead. For example, if expiry is at 3 days, we can send
- // reports at the 2 day deadline and the expiry deadline instead of at the 7
- // day deadline.
- if (expiry_deadline > kReportingWindowDeadlines[0] &&
- expiry_deadline < deadline_to_use) {
- deadline_to_use = expiry_deadline;
- }
-
- return report.impression.impression_time() + deadline_to_use +
- kWindowDeadlineOffset;
-}
-
-int ConversionPolicy::GetMaxConversionsPerImpression() const {
- return 3;
-}
-
-void ConversionPolicy::AssignAttributionCredits(
- std::vector<ConversionReport>* reports) const {
- DCHECK(!reports->empty());
- ConversionReport* last_report = &(reports->at(0));
-
- // Give the latest impression an attribution of 100 and all the rest 0.
- for (auto& report : *reports) {
- report.attribution_credit = 0;
- if (report.impression.impression_time() >
- last_report->impression.impression_time())
- last_report = &report;
- }
-
- last_report->attribution_credit = 100;
-}
-
std::string ConversionPolicy::GetSanitizedConversionData(
uint64_t conversion_data) const {
// Add noise to the conversion when the value is first sanitized from a
// conversion registration event. This noised data will be used for all
// associated impressions that convert.
- conversion_data = noise_provider_->GetNoisedConversionData(conversion_data);
+ if (noise_provider_)
+ conversion_data = noise_provider_->GetNoisedConversionData(conversion_data);
// Allow at most 3 bits of entropy in conversion data. base::StringPrintf() is
// used over base::HexEncode() because HexEncode() returns a hex string with
@@ -141,4 +67,38 @@ std::string ConversionPolicy::GetSanitizedConversionData(
conversion_data % kMaxAllowedConversionValues);
}
+std::string ConversionPolicy::GetSanitizedImpressionData(
+ uint64_t impression_data) const {
+ // Impression data is allowed the full 64 bits.
+ return base::StringPrintf("%" PRIx64, impression_data);
+}
+
+base::Time ConversionPolicy::GetExpiryTimeForImpression(
+ const base::Optional<base::TimeDelta>& declared_expiry,
+ base::Time impression_time) const {
+ static constexpr base::TimeDelta kDefaultImpressionExpiry =
+ base::TimeDelta::FromDays(30);
+
+ // Default to the maximum expiry time.
+ base::TimeDelta expiry = declared_expiry.value_or(kDefaultImpressionExpiry);
+
+ // If the impression specified its own expiry, clamp it to the maximum.
+ return impression_time + std::min(expiry, kDefaultImpressionExpiry);
+}
+
+base::Time ConversionPolicy::GetReportTimeForExpiredReportAtStartup(
+ base::Time now) const {
+ // Do not use any delay in debug mode.
+ if (debug_mode_)
+ return now;
+
+ // Add uniform random noise in the range of [0, 5 minutes] to the report time.
+ // TODO(https://crbug.com/1075600): This delay is very conservative. Consider
+ // increasing this delay once we can be sure reports are still sent at
+ // reasonable times, and not delayed for many browser sessions due to short
+ // session up-times.
+ return now +
+ base::TimeDelta::FromMilliseconds(base::RandInt(0, 5 * 60 * 1000));
+}
+
} // namespace content
diff --git a/chromium/content/browser/conversions/conversion_policy.h b/chromium/content/browser/conversions/conversion_policy.h
index 1a14eac950e..79f86a739ec 100644
--- a/chromium/content/browser/conversions/conversion_policy.h
+++ b/chromium/content/browser/conversions/conversion_policy.h
@@ -10,8 +10,8 @@
#include <string>
#include <vector>
+#include "base/optional.h"
#include "base/time/time.h"
-#include "content/browser/conversions/conversion_report.h"
#include "content/common/content_export.h"
namespace content {
@@ -36,35 +36,41 @@ class CONTENT_EXPORT ConversionPolicy {
static std::unique_ptr<ConversionPolicy> CreateForTesting(
std::unique_ptr<NoiseProvider> noise_provider);
- ConversionPolicy();
+ // |debug_mode| indicates whether the API is currently running in a mode where
+ // it should not use noise.
+ explicit ConversionPolicy(bool debug_mode = false);
ConversionPolicy(const ConversionPolicy& other) = delete;
ConversionPolicy& operator=(const ConversionPolicy& other) = delete;
virtual ~ConversionPolicy();
- // Get the time a conversion report should be sent, by batching reports into
- // set reporting windows based on their impression time. This strictly delays
- // the time a report will be sent.
- virtual base::Time GetReportTimeForConversion(
- const ConversionReport& report) const;
-
- // Maximum number of times the an impression is allowed to convert.
- virtual int GetMaxConversionsPerImpression() const;
-
- // Given a set of conversion reports for a single conversion registrations,
- // assigns attribution credits to each one which will be sent at report time.
- // By default, this performs "last click" attribution which assigns the report
- // for the most recent impression a credit of 100, and the rest a credit of 0.
- virtual void AssignAttributionCredits(
- std::vector<ConversionReport>* reports) const;
-
// Gets the sanitized conversion data for a conversion. This strips entropy
// from the provided to data to at most 3 bits of information.
virtual std::string GetSanitizedConversionData(
uint64_t conversion_data) const;
+ // Gets the sanitized impression data for an impression. Returns the decoded
+ // number as a hexadecimal string.
+ virtual std::string GetSanitizedImpressionData(
+ uint64_t impression_data) const;
+
+ // Returns the expiry time for an impression that is clamped to a maximum
+ // value of 30 days from |impression_time|.
+ virtual base::Time GetExpiryTimeForImpression(
+ const base::Optional<base::TimeDelta>& declared_expiry,
+ base::Time impression_time) const;
+
+ // Delays reports that should have been sent while the browser was not open by
+ // given them a noisy report time to help disassociate them from other
+ // reports.
+ virtual base::Time GetReportTimeForExpiredReportAtStartup(
+ base::Time now) const;
+
private:
// For testing only.
- ConversionPolicy(std::unique_ptr<NoiseProvider> noise_provider);
+ explicit ConversionPolicy(std::unique_ptr<NoiseProvider> noise_provider);
+
+ // Whether the API is running in debug mode. No noise or delay should be used.
+ const bool debug_mode_;
std::unique_ptr<NoiseProvider> noise_provider_;
};
diff --git a/chromium/content/browser/conversions/conversion_policy_unittest.cc b/chromium/content/browser/conversions/conversion_policy_unittest.cc
index b2e5941da25..15ff3505fb7 100644
--- a/chromium/content/browser/conversions/conversion_policy_unittest.cc
+++ b/chromium/content/browser/conversions/conversion_policy_unittest.cc
@@ -16,17 +16,6 @@ namespace content {
namespace {
-constexpr base::TimeDelta kDefaultExpiry = base::TimeDelta::FromDays(30);
-
-ConversionReport GetReport(base::Time impression_time,
- base::Time conversion_time,
- base::TimeDelta expiry = kDefaultExpiry) {
- return ConversionReport(
- ImpressionBuilder(impression_time).SetExpiry(expiry).Build(),
- /*conversion_data=*/"123", conversion_time,
- /*conversion_id=*/base::nullopt);
-}
-
// Fake ConversionNoiseProvider that return un-noised conversion data.
class EmptyNoiseProvider : public ConversionPolicy::NoiseProvider {
public:
@@ -59,6 +48,15 @@ TEST_F(ConversionPolicyTest, HighEntropyConversionData_StrippedToLowerBits) {
->GetSanitizedConversionData(conversion_data));
}
+TEST_F(ConversionPolicyTest, SanitizeHighEntropyImpressionData_Unchanged) {
+ uint64_t impression_data = 256LU;
+
+ // The policy should not alter the impression data, and return the hexadecimal
+ // representation.
+ EXPECT_EQ("100",
+ ConversionPolicy().GetSanitizedImpressionData(impression_data));
+}
+
TEST_F(ConversionPolicyTest, ThreeBitConversionData_Unchanged) {
std::unique_ptr<ConversionPolicy> policy = ConversionPolicy::CreateForTesting(
std::make_unique<EmptyNoiseProvider>());
@@ -75,100 +73,37 @@ TEST_F(ConversionPolicyTest, SantizizeConversionData_OutputHasNoise) {
->GetSanitizedConversionData(4UL));
}
-TEST_F(ConversionPolicyTest, ImmediateConversion_FirstWindowUsed) {
- base::Time impression_time = base::Time::Now();
- auto report = GetReport(impression_time, /*conversion_time=*/impression_time);
- EXPECT_EQ(impression_time + base::TimeDelta::FromDays(2),
- ConversionPolicy().GetReportTimeForConversion(report));
-}
-
-TEST_F(ConversionPolicyTest, ConversionImmediatelyBeforeWindow_NextWindowUsed) {
- base::Time impression_time = base::Time::Now();
- base::Time conversion_time = impression_time + base::TimeDelta::FromDays(2) -
- base::TimeDelta::FromMinutes(1);
- auto report = GetReport(impression_time, conversion_time);
- EXPECT_EQ(impression_time + base::TimeDelta::FromDays(7),
- ConversionPolicy().GetReportTimeForConversion(report));
-}
-
-TEST_F(ConversionPolicyTest, ConversionBeforeWindowDelay_WindowUsed) {
- base::Time impression_time = base::Time::Now();
-
- // The deadline for a window is 1 hour before the window. Use a time just
- // before the deadline.
- base::Time conversion_time = impression_time + base::TimeDelta::FromDays(2) -
- base::TimeDelta::FromMinutes(61);
- auto report = GetReport(impression_time, conversion_time);
- EXPECT_EQ(impression_time + base::TimeDelta::FromDays(2),
- ConversionPolicy().GetReportTimeForConversion(report));
+// This test will fail flakily if noise is used.
+TEST_F(ConversionPolicyTest, DebugMode_ConversionDataNotNoised) {
+ uint64_t conversion_data = 0UL;
+ for (int i = 0; i < 100; i++) {
+ EXPECT_EQ(base::NumberToString(conversion_data),
+ ConversionPolicy(true /* debug_mode */)
+ .GetSanitizedConversionData(conversion_data));
+ }
}
-TEST_F(ConversionPolicyTest,
- ImpressionExpiryBeforeTwoDayWindow_TwoDayWindowUsed) {
+TEST_F(ConversionPolicyTest, NoExpiryForImpression_DefaultUsed) {
base::Time impression_time = base::Time::Now();
- base::Time conversion_time = impression_time + base::TimeDelta::FromHours(1);
-
- // Set the impression to expire before the two day window.
- auto report = GetReport(impression_time, conversion_time,
- /*expiry=*/base::TimeDelta::FromHours(2));
- EXPECT_EQ(impression_time + base::TimeDelta::FromDays(2),
- ConversionPolicy().GetReportTimeForConversion(report));
+ EXPECT_EQ(impression_time + base::TimeDelta::FromDays(30),
+ ConversionPolicy().GetExpiryTimeForImpression(
+ /*declared_expiry=*/base::nullopt, impression_time));
}
-TEST_F(ConversionPolicyTest,
- ImpressionExpiryBeforeSevenDayWindow_ExpiryWindowUsed) {
+TEST_F(ConversionPolicyTest, LargeImpressionExpirySpecified_ClampedTo30Days) {
+ constexpr base::TimeDelta declared_expiry = base::TimeDelta::FromDays(60);
base::Time impression_time = base::Time::Now();
- base::Time conversion_time = impression_time + base::TimeDelta::FromDays(3);
-
- // Set the impression to expire before the two day window.
- auto report = GetReport(impression_time, conversion_time,
- /*expiry=*/base::TimeDelta::FromDays(4));
-
- // The expiry window is reported one hour after expiry time.
- EXPECT_EQ(impression_time + base::TimeDelta::FromDays(4) +
- base::TimeDelta::FromHours(1),
- ConversionPolicy().GetReportTimeForConversion(report));
+ EXPECT_EQ(impression_time + base::TimeDelta::FromDays(30),
+ ConversionPolicy().GetExpiryTimeForImpression(declared_expiry,
+ impression_time));
}
-TEST_F(ConversionPolicyTest,
- ImpressionExpiryAfterSevenDayWindow_ExpiryWindowUsed) {
+TEST_F(ConversionPolicyTest, ImpressionExpirySpecified_ExpiryOverrideDefault) {
+ constexpr base::TimeDelta declared_expiry = base::TimeDelta::FromDays(10);
base::Time impression_time = base::Time::Now();
- base::Time conversion_time = impression_time + base::TimeDelta::FromDays(7);
-
- // Set the impression to expire before the two day window.
- auto report = GetReport(impression_time, conversion_time,
- /*expiry=*/base::TimeDelta::FromDays(9));
-
- // The expiry window is reported one hour after expiry time.
- EXPECT_EQ(impression_time + base::TimeDelta::FromDays(9) +
- base::TimeDelta::FromHours(1),
- ConversionPolicy().GetReportTimeForConversion(report));
-}
-
-TEST_F(ConversionPolicyTest,
- SingleReportForConversion_AttributionCreditAssigned) {
- base::Time now = base::Time::Now();
- std::vector<ConversionReport> reports = {
- GetReport(/*impression_time=*/now, /*conversion_time=*/now)};
- ConversionPolicy().AssignAttributionCredits(&reports);
- EXPECT_EQ(1u, reports.size());
- EXPECT_EQ(100, reports[0].attribution_credit);
-}
-
-TEST_F(ConversionPolicyTest, TwoReportsForConversion_LastReceivesCredit) {
- base::Time now = base::Time::Now();
- std::vector<ConversionReport> reports = {
- GetReport(/*impression_time=*/now, /*conversion_time=*/now),
- GetReport(/*impression_time=*/now + base::TimeDelta::FromHours(100),
- /*conversion_time=*/now)};
- ConversionPolicy().AssignAttributionCredits(&reports);
- EXPECT_EQ(2u, reports.size());
- EXPECT_EQ(0, reports[0].attribution_credit);
- EXPECT_EQ(100, reports[1].attribution_credit);
-
- // Ensure the reports were not rearranged.
- EXPECT_EQ(now + base::TimeDelta::FromHours(100),
- reports[1].impression.impression_time());
+ EXPECT_EQ(impression_time + base::TimeDelta::FromDays(10),
+ ConversionPolicy().GetExpiryTimeForImpression(declared_expiry,
+ impression_time));
}
} // namespace content
diff --git a/chromium/content/browser/conversions/conversion_registration_browsertest.cc b/chromium/content/browser/conversions/conversion_registration_browsertest.cc
index 55d40c22149..8b6fce9261d 100644
--- a/chromium/content/browser/conversions/conversion_registration_browsertest.cc
+++ b/chromium/content/browser/conversions/conversion_registration_browsertest.cc
@@ -10,6 +10,7 @@
#include "content/browser/conversions/conversion_host.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/common/content_features.h"
+#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
@@ -135,6 +136,25 @@ IN_PROC_BROWSER_TEST_F(ConversionRegistrationBrowserTest,
}
IN_PROC_BROWSER_TEST_F(ConversionRegistrationBrowserTest,
+ FeaturePolicyDisabled_ConversionNotRegistered) {
+ EXPECT_TRUE(NavigateToURL(
+ shell(), embedded_test_server()->GetURL(
+ "/page_with_conversion_measurement_disabled.html")));
+ std::unique_ptr<TestConversionHost> host =
+ TestConversionHost::ReplaceAndGetConversionHost(web_contents());
+
+ GURL redirect_url = embedded_test_server()->GetURL(
+ "/server-redirect?" + kWellKnownUrl + "?conversion-data=200");
+ ResourceLoadObserver load_observer(shell());
+ EXPECT_TRUE(ExecJs(web_contents(),
+ JsReplace("createTrackingPixel($1);", redirect_url)));
+ load_observer.WaitForResourceCompletion(redirect_url);
+
+ EXPECT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
+ EXPECT_EQ(0u, host->num_conversions());
+}
+
+IN_PROC_BROWSER_TEST_F(ConversionRegistrationBrowserTest,
ConversionRegistrationNotRedirect_NotReceived) {
EXPECT_TRUE(NavigateToURL(
shell(),
diff --git a/chromium/content/browser/conversions/conversion_report.cc b/chromium/content/browser/conversions/conversion_report.cc
index 8b285ad5791..6d8bbbaf2dc 100644
--- a/chromium/content/browser/conversions/conversion_report.cc
+++ b/chromium/content/browser/conversions/conversion_report.cc
@@ -28,6 +28,7 @@ std::ostream& operator<<(std::ostream& out, const ConversionReport& report) {
<< ", reporting_origin: " << report.impression.reporting_origin()
<< ", conversion_data: " << report.conversion_data
<< ", report_time: " << report.report_time
+ << ", extra_delay: " << report.extra_delay
<< ", attribution_credit: " << report.attribution_credit;
return out;
}
diff --git a/chromium/content/browser/conversions/conversion_report.h b/chromium/content/browser/conversions/conversion_report.h
index 64d2e62f0ef..38aa9b4a5a1 100644
--- a/chromium/content/browser/conversions/conversion_report.h
+++ b/chromium/content/browser/conversions/conversion_report.h
@@ -38,6 +38,10 @@ struct CONTENT_EXPORT ConversionReport {
// The time this conversion report should be sent.
base::Time report_time;
+ // Tracks ephemeral increases to |report_time| for this conversion report, for
+ // the purposes of logging metrics.
+ base::TimeDelta extra_delay;
+
// 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
diff --git a/chromium/content/browser/conversions/conversion_reporter_impl.cc b/chromium/content/browser/conversions/conversion_reporter_impl.cc
new file mode 100644
index 00000000000..3011e2efc51
--- /dev/null
+++ b/chromium/content/browser/conversions/conversion_reporter_impl.cc
@@ -0,0 +1,108 @@
+// 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_reporter_impl.h"
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/rand_util.h"
+#include "base/time/clock.h"
+#include "content/browser/conversions/conversion_manager.h"
+#include "content/browser/conversions/conversion_network_sender_impl.h"
+
+namespace content {
+
+ConversionReporterImpl::ConversionReporterImpl(
+ StoragePartition* storage_partition,
+ const base::Clock* clock)
+ : clock_(clock),
+ network_sender_(
+ std::make_unique<ConversionNetworkSenderImpl>(storage_partition)) {}
+
+ConversionReporterImpl::~ConversionReporterImpl() = default;
+
+void ConversionReporterImpl::AddReportsToQueue(
+ std::vector<ConversionReport> reports,
+ base::RepeatingCallback<void(int64_t)> report_sent_callback) {
+ DCHECK(!reports.empty());
+
+ std::vector<std::unique_ptr<ConversionReport>> swappable_reports;
+ for (ConversionReport& report : reports) {
+ swappable_reports.push_back(
+ std::make_unique<ConversionReport>(std::move(report)));
+ }
+
+ // Shuffle new reports to provide plausible deniability on the ordering of
+ // reports that share the same |report_time|. This is important because
+ // multiple conversions for the same impression share the same report time if
+ // they are within the same reporting window, and we do not want to allow
+ // ordering on their conversion metadata bits.
+ base::RandomShuffle(swappable_reports.begin(), swappable_reports.end());
+
+ for (std::unique_ptr<ConversionReport>& report : swappable_reports) {
+ // If the given report is already being processed, ignore it.
+ bool inserted = conversion_report_callbacks_
+ .emplace(*(report->conversion_id), report_sent_callback)
+ .second;
+ if (inserted)
+ report_queue_.push(std::move(report));
+ }
+ MaybeScheduleNextReport();
+}
+
+void ConversionReporterImpl::SetNetworkSenderForTesting(
+ std::unique_ptr<NetworkSender> network_sender) {
+ network_sender_ = std::move(network_sender);
+}
+
+void ConversionReporterImpl::SendNextReport() {
+ // Send the next report and remove it from the queue. Bind the conversion id
+ // to the sent callback so we know which conversion report has finished
+ // sending.
+ network_sender_->SendReport(
+ report_queue_.top().get(),
+ base::BindOnce(&ConversionReporterImpl::OnReportSent,
+ base::Unretained(this),
+ *report_queue_.top()->conversion_id));
+ report_queue_.pop();
+ MaybeScheduleNextReport();
+}
+
+void ConversionReporterImpl::MaybeScheduleNextReport() {
+ if (report_queue_.empty())
+ return;
+
+ send_report_timer_.Stop();
+ base::Time current_time = clock_->Now();
+ base::Time report_time = report_queue_.top()->report_time;
+
+ // Start a timer to wait until the next report is ready to be sent. This
+ // purposefully yields the thread for every report that gets scheduled.
+ // Unretained is safe because the task should never actually be posted if the
+ // timer itself is destroyed
+ send_report_timer_.Start(
+ FROM_HERE,
+ (report_time < current_time) ? base::TimeDelta()
+ : report_time - current_time,
+ base::BindOnce(&ConversionReporterImpl::SendNextReport,
+ base::Unretained(this)));
+}
+
+void ConversionReporterImpl::OnReportSent(int64_t conversion_id) {
+ auto it = conversion_report_callbacks_.find(conversion_id);
+ DCHECK(it != conversion_report_callbacks_.end());
+ std::move(it->second).Run(conversion_id);
+ conversion_report_callbacks_.erase(it);
+}
+
+bool ConversionReporterImpl::ReportComparator::operator()(
+ const std::unique_ptr<ConversionReport>& a,
+ const std::unique_ptr<ConversionReport>& b) const {
+ // Returns whether a should appear before b in ordering. Because
+ // std::priority_queue is max priority queue, we used greater then to make a
+ // min priority queue.
+ return a->report_time > b->report_time;
+}
+
+} // namespace content
diff --git a/chromium/content/browser/conversions/conversion_reporter_impl.h b/chromium/content/browser/conversions/conversion_reporter_impl.h
new file mode 100644
index 00000000000..855a2b3a4f4
--- /dev/null
+++ b/chromium/content/browser/conversions/conversion_reporter_impl.h
@@ -0,0 +1,111 @@
+// 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_REPORTER_IMPL_H_
+#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_REPORTER_IMPL_H_
+
+#include <stdint.h>
+#include <memory>
+#include <queue>
+#include <vector>
+
+#include "base/callback.h"
+#include "base/containers/flat_map.h"
+#include "base/time/time.h"
+#include "base/timer/timer.h"
+#include "content/browser/conversions/conversion_manager_impl.h"
+#include "content/browser/conversions/conversion_report.h"
+#include "content/common/content_export.h"
+#include "services/network/public/cpp/shared_url_loader_factory.h"
+
+namespace base {
+class Clock;
+} // namespace base
+
+namespace content {
+
+class StoragePartition;
+
+// This class is responsible for managing the dispatch of conversion reports to
+// a ConversionReporterImpl::NetworkSender. It maintains a queue of reports and
+// a timer to ensure all reports are sent at the correct time, since the time in
+// which a conversion report is sent is potentially sensitive information.
+// Created and owned by ConversionManager.
+class CONTENT_EXPORT ConversionReporterImpl
+ : public ConversionManagerImpl::ConversionReporter {
+ public:
+ // This class is responsible for sending conversion reports to their
+ // configured endpoints over the network.
+ class NetworkSender {
+ public:
+ virtual ~NetworkSender() = default;
+
+ // Callback used to notify caller that the requested report has been sent.
+ using ReportSentCallback = base::OnceCallback<void()>;
+
+ // Generates and sends a conversion report matching |report|. This should
+ // generate a secure POST quest with no-credentials. Does not persist the
+ // raw pointer.
+ virtual void SendReport(ConversionReport* report,
+ ReportSentCallback sent_callback) = 0;
+ };
+
+ ConversionReporterImpl(StoragePartition* storage_partition,
+ const base::Clock* clock);
+ ConversionReporterImpl(const ConversionReporterImpl&) = delete;
+ ConversionReporterImpl& operator=(const ConversionReporterImpl&) = delete;
+ ~ConversionReporterImpl() override;
+
+ // ConversionManagerImpl::ConversionReporter:
+ void AddReportsToQueue(
+ std::vector<ConversionReport> reports,
+ base::RepeatingCallback<void(int64_t)> report_sent_callback) override;
+
+ void SetNetworkSenderForTesting(
+ std::unique_ptr<NetworkSender> network_sender);
+
+ private:
+ void MaybeScheduleNextReport();
+ void SendNextReport();
+
+ // Called when a conversion report sent via NetworkSender::SendReport() has
+ // completed loading.
+ void OnReportSent(int64_t conversion_id);
+
+ // Comparator used to order ConversionReports by their report time, with the
+ // smallest time at the top of |report_queue_|.
+ struct ReportComparator {
+ bool operator()(const std::unique_ptr<ConversionReport>& a,
+ const std::unique_ptr<ConversionReport>& b) const;
+ };
+
+ // Priority queue which holds reports that are yet to be sent. Reports are
+ // removed from the queue when they are delivered to the NetworkSender.
+ std::priority_queue<std::unique_ptr<ConversionReport>,
+ std::vector<std::unique_ptr<ConversionReport>>,
+ ReportComparator>
+ report_queue_;
+
+ // Map of all conversion ids that are currently in |report_queue| or are being
+ // sent by |network_sender_|, and their associated report sent callbacks. The
+ // number of concurrent conversion reports being sent at any time is expected
+ // to be small, so a flat_map is used.
+ base::flat_map<int64_t, base::OnceCallback<void(int64_t)>>
+ conversion_report_callbacks_;
+
+ const base::Clock* clock_;
+
+ // Timer which signals the next report in |report_queue_| should be sent.
+ base::OneShotTimer send_report_timer_;
+
+ // Responsible for issuing requests to network for report that need to be
+ // sent. Calls OnReportSent() when a report has finished sending.
+ //
+ // Should never be nullptr.
+ std::unique_ptr<NetworkSender> network_sender_;
+};
+
+} // namespace content
+
+#endif // CONTENT_BROWSER_CONVERSIONS_CONVERSION_REPORTER_IMPL_H_
diff --git a/chromium/content/browser/conversions/conversion_reporter_impl_unittest.cc b/chromium/content/browser/conversions/conversion_reporter_impl_unittest.cc
new file mode 100644
index 00000000000..a2b030ed8f6
--- /dev/null
+++ b/chromium/content/browser/conversions/conversion_reporter_impl_unittest.cc
@@ -0,0 +1,203 @@
+// 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_reporter_impl.h"
+
+#include <stdint.h>
+
+#include "base/bind.h"
+#include "base/sequenced_task_runner.h"
+#include "base/strings/strcat.h"
+#include "base/task/post_task.h"
+#include "base/test/bind_test_util.h"
+#include "base/test/simple_test_clock.h"
+#include "content/browser/conversions/conversion_manager.h"
+#include "content/browser/conversions/conversion_test_utils.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/storage_partition.h"
+#include "content/public/test/browser_task_environment.h"
+#include "content/public/test/test_browser_context.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace content {
+
+namespace {
+
+// Create a report which should be sent at |report_time|. Impression
+// data/conversion data/conversion id are all the same for simplicity.
+ConversionReport GetReport(base::Time report_time, int64_t conversion_id) {
+ // Construct impressions with a null impression time as it is not used for
+ // reporting.
+ return ConversionReport(ImpressionBuilder(base::Time()).Build(),
+ /*conversion_data=*/"", report_time,
+ /*conversion_id=*/conversion_id);
+}
+
+// NetworkSender that keep track of the last sent report id.
+class MockNetworkSender : public ConversionReporterImpl::NetworkSender {
+ public:
+ MockNetworkSender() = default;
+
+ void SendReport(ConversionReport* conversion_report,
+ ReportSentCallback sent_callback) override {
+ last_sent_report_id_ = *conversion_report->conversion_id;
+ num_reports_sent_++;
+ std::move(sent_callback).Run();
+ }
+
+ int64_t last_sent_report_id() { return last_sent_report_id_; }
+
+ size_t num_reports_sent() { return num_reports_sent_; }
+
+ private:
+ size_t num_reports_sent_ = 0u;
+ int64_t last_sent_report_id_ = -1;
+};
+
+} // namespace
+
+class ConversionReporterImplTest : public testing::Test {
+ public:
+ ConversionReporterImplTest()
+ : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME),
+ browser_context_(std::make_unique<TestBrowserContext>()),
+ reporter_(std::make_unique<ConversionReporterImpl>(
+ BrowserContext::GetDefaultStoragePartition(browser_context_.get()),
+ task_environment_.GetMockClock())) {
+ auto network_sender = std::make_unique<MockNetworkSender>();
+ sender_ = network_sender.get();
+ reporter_->SetNetworkSenderForTesting(std::move(network_sender));
+ }
+
+ const base::Clock& clock() { return *task_environment_.GetMockClock(); }
+
+ protected:
+ // |task_enviorment_| must be initialized first.
+ content::BrowserTaskEnvironment task_environment_;
+ std::unique_ptr<TestBrowserContext> browser_context_;
+
+ std::unique_ptr<ConversionReporterImpl> reporter_;
+ MockNetworkSender* sender_;
+};
+
+TEST_F(ConversionReporterImplTest,
+ ReportAddedWithImmediateReportTime_ReportSent) {
+ reporter_->AddReportsToQueue({GetReport(clock().Now(), /*conversion_id=*/1)},
+ base::BindRepeating([](int64_t conversion_id) {
+ EXPECT_EQ(1L, conversion_id);
+ }));
+
+ // Fast forward by 0, as we yield the thread when a report is scheduled to be
+ // sent.
+ task_environment_.FastForwardBy(base::TimeDelta());
+ EXPECT_EQ(1, sender_->last_sent_report_id());
+}
+
+TEST_F(ConversionReporterImplTest,
+ ReportWithReportTimeBeforeCurrentTime_ReportSent) {
+ reporter_->AddReportsToQueue(
+ {GetReport(clock().Now() - base::TimeDelta::FromHours(10),
+ /*conversion_id=*/1)},
+ base::BindRepeating(
+ [](int64_t conversion_id) { EXPECT_EQ(1L, conversion_id); }));
+
+ // Fast forward by 0, as we yield the thread when a report is scheduled to be
+ // sent.
+ task_environment_.FastForwardBy(base::TimeDelta());
+ EXPECT_EQ(1, sender_->last_sent_report_id());
+}
+
+TEST_F(ConversionReporterImplTest,
+ ReportWithDelayedReportTime_NotSentUntilDelay) {
+ const base::TimeDelta delay = base::TimeDelta::FromMinutes(30);
+
+ reporter_->AddReportsToQueue(
+ {GetReport(clock().Now() + delay, /*conversion_id=*/1)},
+ base::DoNothing());
+ task_environment_.FastForwardBy(base::TimeDelta());
+ EXPECT_EQ(0u, sender_->num_reports_sent());
+
+ task_environment_.FastForwardBy(delay - base::TimeDelta::FromSeconds(1));
+ EXPECT_EQ(0u, sender_->num_reports_sent());
+
+ task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(1));
+ EXPECT_EQ(1u, sender_->num_reports_sent());
+}
+
+TEST_F(ConversionReporterImplTest, DuplicateReportScheduled_Ignored) {
+ reporter_->AddReportsToQueue(
+ {GetReport(clock().Now() + base::TimeDelta::FromMinutes(1),
+ /*conversion_id=*/1)},
+ base::DoNothing());
+
+ // A duplicate report should not be scheduled.
+ reporter_->AddReportsToQueue(
+ {GetReport(clock().Now() + base::TimeDelta::FromMinutes(1),
+ /*conversion_id=*/1)},
+ base::DoNothing());
+ task_environment_.FastForwardBy(base::TimeDelta::FromMinutes(1));
+ EXPECT_EQ(1u, sender_->num_reports_sent());
+}
+
+TEST_F(ConversionReporterImplTest,
+ NewReportWithPreviouslySeenConversionId_Scheduled) {
+ reporter_->AddReportsToQueue({GetReport(clock().Now(), /*conversion_id=*/1)},
+ base::DoNothing());
+ task_environment_.FastForwardBy(base::TimeDelta());
+ EXPECT_EQ(1u, sender_->num_reports_sent());
+
+ // We should schedule the new report because the previous report has been
+ // sent.
+ reporter_->AddReportsToQueue({GetReport(clock().Now(), /*conversion_id=*/1)},
+ base::DoNothing());
+ task_environment_.FastForwardBy(base::TimeDelta());
+ EXPECT_EQ(2u, sender_->num_reports_sent());
+}
+
+TEST_F(ConversionReporterImplTest, ManyReportsAddedAtOnce_SentInOrder) {
+ std::vector<ConversionReport> reports;
+ int64_t last_report_id = 0UL;
+ for (int i = 1; i < 10; i++) {
+ reports.push_back(GetReport(clock().Now() + base::TimeDelta::FromMinutes(i),
+ /*conversion_id=*/i));
+ }
+ reporter_->AddReportsToQueue(
+ reports, base::BindLambdaForTesting([&](int64_t conversion_id) {
+ last_report_id = conversion_id;
+ }));
+ task_environment_.FastForwardBy(base::TimeDelta());
+ EXPECT_EQ(0u, sender_->num_reports_sent());
+
+ for (int i = 1; i < 10; i++) {
+ task_environment_.FastForwardBy(base::TimeDelta::FromMinutes(1));
+
+ EXPECT_EQ(static_cast<size_t>(i), sender_->num_reports_sent());
+ EXPECT_EQ(static_cast<int64_t>(i), sender_->last_sent_report_id());
+ EXPECT_EQ(static_cast<int64_t>(i), last_report_id);
+ }
+}
+
+TEST_F(ConversionReporterImplTest, ManyReportsAddedSeparately_SentInOrder) {
+ int64_t last_report_id = 0;
+ auto report_sent_callback = base::BindLambdaForTesting(
+ [&](int64_t conversion_id) { last_report_id = conversion_id; });
+ for (int i = 1; i < 10; i++) {
+ reporter_->AddReportsToQueue(
+ {GetReport(clock().Now() + base::TimeDelta::FromMinutes(i),
+ /*conversion_id=*/i)},
+ report_sent_callback);
+ }
+ task_environment_.FastForwardBy(base::TimeDelta());
+ EXPECT_EQ(0u, sender_->num_reports_sent());
+
+ for (int i = 1; i < 10; i++) {
+ task_environment_.FastForwardBy(base::TimeDelta::FromMinutes(1));
+
+ EXPECT_EQ(static_cast<size_t>(i), sender_->num_reports_sent());
+ EXPECT_EQ(static_cast<int64_t>(i), sender_->last_sent_report_id());
+ EXPECT_EQ(static_cast<int64_t>(i), last_report_id);
+ }
+}
+
+} // namespace content
diff --git a/chromium/content/browser/conversions/conversion_storage.h b/chromium/content/browser/conversions/conversion_storage.h
index cfdae6e57ce..1c8feed1ccf 100644
--- a/chromium/content/browser/conversions/conversion_storage.h
+++ b/chromium/content/browser/conversions/conversion_storage.h
@@ -8,10 +8,12 @@
#include <stdint.h>
#include <vector>
+#include "base/callback.h"
#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"
+#include "url/origin.h"
namespace content {
@@ -41,6 +43,21 @@ class ConversionStorage {
// Impressions will be checked against this limit after they schedule a new
// report.
virtual int GetMaxConversionsPerImpression() const = 0;
+
+ // These limits are designed solely to avoid excessive disk / memory usage.
+ // In particular, they do not correspond with any privacy parameters.
+ // TODO(crbug.com/1082754): Consider replacing this functionality (and the
+ // data deletion logic) with the quota system.
+ //
+ // Returns the maximum number of impressions that can be in storage at any
+ // time for an impression top-level origin.
+ virtual int GetMaxImpressionsPerOrigin() const = 0;
+ // Returns the maximum number of conversions that can be in storage at any
+ // time for a conversion top-level origin. Note that since reporting
+ // origins are the actual entities that invoke conversion registration, we
+ // could consider changing this limit to be keyed by a <conversion origin,
+ // reporting origin> tuple.
+ virtual int GetMaxConversionsPerOrigin() const = 0;
};
virtual ~ConversionStorage() = default;
@@ -69,6 +86,13 @@ class ConversionStorage {
virtual std::vector<ConversionReport> GetConversionsToReport(
base::Time max_report_time) = 0;
+ // Returns all active impressions in storage. Active impressions are all
+ // impressions that can still convert. Impressions that: are past expiry,
+ // reached the conversion limit, or was marked inactive due to having
+ // converted and then superceded by a matching impression should not be
+ // returned.
+ virtual std::vector<StorableImpression> GetActiveImpressions() = 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;
@@ -77,9 +101,18 @@ class ConversionStorage {
// 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.
+ // Deletes all data in storage for URLs matching |filter|, between
+ // |delete_begin| and |delete_end| time. More specifically, this:
+ // 1. Deletes all impressions within the time range. If any conversion is
+ // attributed to this impression it is also deleted.
+ // 2. Deletes all conversions within the time range. All impressions
+ // attributed to the conversion are also deleted.
+ //
+ // Note: if |filter| is null, it means that all Origins should match.
+ virtual void ClearData(
+ base::Time delete_begin,
+ base::Time delete_end,
+ base::RepeatingCallback<bool(const url::Origin& origin)> filter) = 0;
};
} // namespace content
diff --git a/chromium/content/browser/conversions/conversion_storage_delegate_impl.cc b/chromium/content/browser/conversions/conversion_storage_delegate_impl.cc
new file mode 100644
index 00000000000..6899b7ef7f8
--- /dev/null
+++ b/chromium/content/browser/conversions/conversion_storage_delegate_impl.cc
@@ -0,0 +1,110 @@
+// 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_delegate_impl.h"
+
+namespace content {
+
+ConversionStorageDelegateImpl::ConversionStorageDelegateImpl(bool debug_mode)
+ : debug_mode_(debug_mode) {
+ DETACH_FROM_SEQUENCE(sequence_checker_);
+}
+
+void ConversionStorageDelegateImpl::ProcessNewConversionReports(
+ std::vector<ConversionReport>* reports) {
+ DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+ DCHECK(!reports->empty());
+ ConversionReport* last_report = &(reports->at(0));
+
+ // Assign attribution credits to each report that will be sent at report time.
+ // This performs "last click" attribution which assigns the report
+ // for the most recent impression a credit of 100, and the rest a credit of 0.
+ for (ConversionReport& report : *reports) {
+ report.report_time = GetReportTimeForConversion(report);
+
+ report.attribution_credit = 0;
+ if (report.impression.impression_time() >
+ last_report->impression.impression_time())
+ last_report = &report;
+ }
+
+ last_report->attribution_credit = 100;
+}
+
+int ConversionStorageDelegateImpl::GetMaxConversionsPerImpression() const {
+ DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+ return 3;
+}
+
+int ConversionStorageDelegateImpl::GetMaxImpressionsPerOrigin() const {
+ return 1024;
+}
+
+int ConversionStorageDelegateImpl::GetMaxConversionsPerOrigin() const {
+ return 1024;
+}
+
+base::Time ConversionStorageDelegateImpl::GetReportTimeForConversion(
+ const ConversionReport& report) const {
+ DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+ // |report.report_time| is roughly ~now, for newly created conversion
+ // reports. If in debug mode, the report should be sent immediately.
+ if (debug_mode_)
+ return report.report_time;
+
+ // After the initial impression, a schedule of reporting windows and deadlines
+ // associated with that impression begins. The time between impression time
+ // and impression expiry is split into multiple reporting windows. At the end
+ // of each window, the browser will send all scheduled reports for that
+ // impression.
+ //
+ // Each reporting window has a deadline and only conversions registered before
+ // that deadline are sent in that window. Each deadline is one hour prior to
+ // the window report time. The deadlines relative to impression time are <2
+ // days minus 1 hour, 7 days minus 1 hour, impression expiry>. The impression
+ // expiry window is only used for conversions that occur after the 7 day
+ // deadline. For example, a conversion which happens one hour after an
+ // impression with an expiry of two hours, is still reported in the 2 day
+ // window.
+ constexpr base::TimeDelta kWindowDeadlineOffset =
+ base::TimeDelta::FromHours(1);
+ base::TimeDelta expiry_deadline =
+ report.impression.expiry_time() - report.impression.impression_time();
+ const base::TimeDelta kReportingWindowDeadlines[] = {
+ base::TimeDelta::FromDays(2) - kWindowDeadlineOffset,
+ base::TimeDelta::FromDays(7) - kWindowDeadlineOffset, expiry_deadline};
+
+ base::TimeDelta deadline_to_use;
+
+ // Given a conversion report that was created at |report.report_time|, find
+ // the first applicable reporting window this conversion should be reported
+ // at.
+ for (base::TimeDelta report_window_deadline : kReportingWindowDeadlines) {
+ // If this window is valid for the conversion, use it. |report.report_time|
+ // is roughly ~now, as the conversion time is used as the default value for
+ // newly created reports that have not had a report time set.
+ if (report.impression.impression_time() + report_window_deadline >=
+ report.report_time) {
+ deadline_to_use = report_window_deadline;
+ break;
+ }
+ }
+
+ // Valid conversion reports should always have a valid reporting deadline.
+ DCHECK(!deadline_to_use.is_zero());
+
+ // If the expiry deadline falls after the first window, but before another
+ // window, use it instead. For example, if expiry is at 3 days, we can send
+ // reports at the 2 day deadline and the expiry deadline instead of at the 7
+ // day deadline.
+ if (expiry_deadline > kReportingWindowDeadlines[0] &&
+ expiry_deadline < deadline_to_use) {
+ deadline_to_use = expiry_deadline;
+ }
+
+ return report.impression.impression_time() + deadline_to_use +
+ kWindowDeadlineOffset;
+}
+
+} // namespace content
diff --git a/chromium/content/browser/conversions/conversion_storage_delegate_impl.h b/chromium/content/browser/conversions/conversion_storage_delegate_impl.h
new file mode 100644
index 00000000000..c0d103a7905
--- /dev/null
+++ b/chromium/content/browser/conversions/conversion_storage_delegate_impl.h
@@ -0,0 +1,53 @@
+// 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_DELEGATE_IMPL_H_
+#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_STORAGE_DELEGATE_IMPL_H_
+
+#include "base/sequence_checker.h"
+#include "base/time/time.h"
+#include "content/browser/conversions/conversion_report.h"
+#include "content/browser/conversions/conversion_storage.h"
+#include "content/common/content_export.h"
+
+namespace content {
+
+// Implementation of the storage delegate. This class handles assigning
+// attribution credits and report times to newly created conversion reports. It
+// also controls constants for ConversionStorage. This is owned by
+// ConversionStorageSql, and should only be accessed on the conversions storage
+// task runner.
+class CONTENT_EXPORT ConversionStorageDelegateImpl
+ : public ConversionStorage::Delegate {
+ public:
+ explicit ConversionStorageDelegateImpl(bool debug_mode = false);
+ ConversionStorageDelegateImpl(const ConversionStorageDelegateImpl& other) =
+ delete;
+ ConversionStorageDelegateImpl& operator=(
+ const ConversionStorageDelegateImpl& other) = delete;
+ ~ConversionStorageDelegateImpl() override = default;
+
+ // ConversionStorageDelegate:
+ void ProcessNewConversionReports(
+ std::vector<ConversionReport>* reports) override;
+ int GetMaxConversionsPerImpression() const override;
+ int GetMaxImpressionsPerOrigin() const override;
+ int GetMaxConversionsPerOrigin() const override;
+
+ private:
+ // Get the time a conversion report should be sent, by batching reports into
+ // set reporting windows based on their impression time. This strictly delays
+ // the time a report will be sent.
+ base::Time GetReportTimeForConversion(const ConversionReport& report) const;
+
+ // Whether the API is running in debug mode, meaning that there should be
+ // no delays or noise added to reports.
+ bool debug_mode_ = false;
+
+ SEQUENCE_CHECKER(sequence_checker_);
+};
+
+} // namespace content
+
+#endif // CONTENT_BROWSER_CONVERSIONS_CONVERSION_STORAGE_DELEGATE_IMPL_H_
diff --git a/chromium/content/browser/conversions/conversion_storage_delegate_impl_unittest.cc b/chromium/content/browser/conversions/conversion_storage_delegate_impl_unittest.cc
new file mode 100644
index 00000000000..bffa131e510
--- /dev/null
+++ b/chromium/content/browser/conversions/conversion_storage_delegate_impl_unittest.cc
@@ -0,0 +1,148 @@
+// 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_delegate_impl.h"
+
+#include <vector>
+
+#include "base/optional.h"
+#include "base/time/time.h"
+#include "content/browser/conversions/conversion_report.h"
+#include "content/browser/conversions/conversion_test_utils.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace content {
+
+namespace {
+
+constexpr base::TimeDelta kDefaultExpiry = base::TimeDelta::FromDays(30);
+
+ConversionReport GetReport(base::Time impression_time,
+ base::Time conversion_time,
+ base::TimeDelta expiry = kDefaultExpiry) {
+ return ConversionReport(
+ ImpressionBuilder(impression_time).SetExpiry(expiry).Build(),
+ /*conversion_data=*/"123", conversion_time,
+ /*conversion_id=*/base::nullopt);
+}
+
+} // namespace
+
+class ConversionStorageDelegateImplTest : public testing::Test {
+ public:
+ ConversionStorageDelegateImplTest() = default;
+};
+
+TEST_F(ConversionStorageDelegateImplTest, ImmediateConversion_FirstWindowUsed) {
+ base::Time impression_time = base::Time::Now();
+ std::vector<ConversionReport> reports = {
+ GetReport(impression_time, /*conversion_time=*/impression_time)};
+ ConversionStorageDelegateImpl().ProcessNewConversionReports(&reports);
+ EXPECT_EQ(impression_time + base::TimeDelta::FromDays(2),
+ reports[0].report_time);
+}
+
+TEST_F(ConversionStorageDelegateImplTest,
+ ConversionImmediatelyBeforeWindow_NextWindowUsed) {
+ base::Time impression_time = base::Time::Now();
+ base::Time conversion_time = impression_time + base::TimeDelta::FromDays(2) -
+ base::TimeDelta::FromMinutes(1);
+ std::vector<ConversionReport> reports = {
+ GetReport(impression_time, conversion_time)};
+ ConversionStorageDelegateImpl().ProcessNewConversionReports(&reports);
+ EXPECT_EQ(impression_time + base::TimeDelta::FromDays(7),
+ reports[0].report_time);
+}
+
+TEST_F(ConversionStorageDelegateImplTest,
+ ConversionBeforeWindowDelay_WindowUsed) {
+ base::Time impression_time = base::Time::Now();
+
+ // The deadline for a window is 1 hour before the window. Use a time just
+ // before the deadline.
+ base::Time conversion_time = impression_time + base::TimeDelta::FromDays(2) -
+ base::TimeDelta::FromMinutes(61);
+ std::vector<ConversionReport> reports = {
+ GetReport(impression_time, conversion_time)};
+ ConversionStorageDelegateImpl().ProcessNewConversionReports(&reports);
+ EXPECT_EQ(impression_time + base::TimeDelta::FromDays(2),
+ reports[0].report_time);
+}
+
+TEST_F(ConversionStorageDelegateImplTest,
+ ImpressionExpiryBeforeTwoDayWindow_TwoDayWindowUsed) {
+ base::Time impression_time = base::Time::Now();
+ base::Time conversion_time = impression_time + base::TimeDelta::FromHours(1);
+
+ // Set the impression to expire before the two day window.
+ std::vector<ConversionReport> reports = {
+ GetReport(impression_time, conversion_time,
+ /*expiry=*/base::TimeDelta::FromHours(2))};
+ ConversionStorageDelegateImpl().ProcessNewConversionReports(&reports);
+ EXPECT_EQ(impression_time + base::TimeDelta::FromDays(2),
+ reports[0].report_time);
+}
+
+TEST_F(ConversionStorageDelegateImplTest,
+ ImpressionExpiryBeforeSevenDayWindow_ExpiryWindowUsed) {
+ base::Time impression_time = base::Time::Now();
+ base::Time conversion_time = impression_time + base::TimeDelta::FromDays(3);
+
+ // Set the impression to expire before the two day window.
+ std::vector<ConversionReport> reports = {
+ GetReport(impression_time, conversion_time,
+ /*expiry=*/base::TimeDelta::FromDays(4))};
+ ConversionStorageDelegateImpl().ProcessNewConversionReports(&reports);
+
+ // The expiry window is reported one hour after expiry time.
+ EXPECT_EQ(impression_time + base::TimeDelta::FromDays(4) +
+ base::TimeDelta::FromHours(1),
+ reports[0].report_time);
+}
+
+TEST_F(ConversionStorageDelegateImplTest,
+ ImpressionExpiryAfterSevenDayWindow_ExpiryWindowUsed) {
+ base::Time impression_time = base::Time::Now();
+ base::Time conversion_time = impression_time + base::TimeDelta::FromDays(7);
+
+ // Set the impression to expire before the two day window.
+ std::vector<ConversionReport> reports = {
+ GetReport(impression_time, conversion_time,
+ /*expiry=*/base::TimeDelta::FromDays(9))};
+ ConversionStorageDelegateImpl().ProcessNewConversionReports(&reports);
+
+ // The expiry window is reported one hour after expiry time.
+ EXPECT_EQ(impression_time + base::TimeDelta::FromDays(9) +
+ base::TimeDelta::FromHours(1),
+ reports[0].report_time);
+}
+
+TEST_F(ConversionStorageDelegateImplTest,
+ SingleReportForConversion_AttributionCreditAssigned) {
+ base::Time now = base::Time::Now();
+ std::vector<ConversionReport> reports = {
+ GetReport(/*impression_time=*/now, /*conversion_time=*/now)};
+ ConversionStorageDelegateImpl().ProcessNewConversionReports(&reports);
+ EXPECT_EQ(1u, reports.size());
+ EXPECT_EQ(100, reports[0].attribution_credit);
+}
+
+TEST_F(ConversionStorageDelegateImplTest,
+ TwoReportsForConversion_LastReceivesCredit) {
+ base::Time now = base::Time::Now();
+ std::vector<ConversionReport> reports = {
+ GetReport(/*impression_time=*/now, /*conversion_time=*/now),
+ GetReport(/*impression_time=*/now + base::TimeDelta::FromHours(100),
+ /*conversion_time=*/now)};
+ ConversionStorageDelegateImpl().ProcessNewConversionReports(&reports);
+ EXPECT_EQ(2u, reports.size());
+ EXPECT_EQ(0, reports[0].attribution_credit);
+ EXPECT_EQ(100, reports[1].attribution_credit);
+
+ // Ensure the reports were not rearranged.
+ EXPECT_EQ(now + base::TimeDelta::FromHours(100),
+ reports[1].impression.impression_time());
+}
+
+} // namespace content
diff --git a/chromium/content/browser/conversions/conversion_storage_sql.cc b/chromium/content/browser/conversions/conversion_storage_sql.cc
index 9f8abdbb270..a921f883991 100644
--- a/chromium/content/browser/conversions/conversion_storage_sql.cc
+++ b/chromium/content/browser/conversions/conversion_storage_sql.cc
@@ -8,8 +8,10 @@
#include <utility>
#include "base/bind.h"
+#include "base/containers/flat_set.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
+#include "base/metrics/histogram_macros.h"
#include "base/optional.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
@@ -28,16 +30,10 @@ 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.
+ // 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.
DCHECK(!origin.opaque());
- DCHECK_EQ(url::kHttpsScheme, origin.scheme());
return origin.Serialize();
}
@@ -58,12 +54,13 @@ base::Time DeserializeTime(int64_t microseconds) {
ConversionStorageSql::ConversionStorageSql(
const base::FilePath& path_to_database_dir,
- Delegate* delegate,
- base::Clock* clock)
+ std::unique_ptr<Delegate> delegate,
+ const base::Clock* clock)
: path_to_database_(path_to_database_dir.Append(kDatabaseName)),
clock_(clock),
- delegate_(delegate),
+ delegate_(std::move(delegate)),
weak_factory_(this) {
+ DCHECK(delegate_);
DETACH_FROM_SEQUENCE(sequence_checker_);
}
@@ -91,6 +88,19 @@ void ConversionStorageSql::StoreImpression(
const StorableImpression& impression) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+ // Cleanup any impression that may be expired by this point. This is done when
+ // an impression is added to prevent additional logic for cleaning the table
+ // while providing a guarantee that the size of the table is proportional to
+ // the number of active impression.
+ DeleteExpiredImpressions();
+
+ // TODO(csharrison): Thread this failure to the caller and report a console
+ // error.
+ const std::string serialized_impression_origin =
+ SerializeOrigin(impression.impression_origin());
+ if (!HasCapacityForStoringImpression(serialized_impression_origin))
+ return;
+
// 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.
@@ -98,6 +108,11 @@ void ConversionStorageSql::StoreImpression(
if (!transaction.Begin())
return;
+ const std::string serialized_conversion_origin =
+ SerializeOrigin(impression.conversion_origin());
+ const std::string serialized_reporting_origin =
+ SerializeOrigin(impression.reporting_origin());
+
// 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.
@@ -107,10 +122,8 @@ void ConversionStorageSql::StoreImpression(
"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.BindString(0, serialized_conversion_origin);
+ deactivate_statement.BindString(1, serialized_reporting_origin);
deactivate_statement.Run();
const char kInsertImpressionSql[] =
@@ -121,9 +134,9 @@ void ConversionStorageSql::StoreImpression(
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.BindString(1, serialized_impression_origin);
+ statement.BindString(2, serialized_conversion_origin);
+ statement.BindString(3, serialized_reporting_origin);
statement.BindInt64(4, SerializeTime(impression.impression_time()));
statement.BindInt64(5, SerializeTime(impression.expiry_time()));
statement.Run();
@@ -136,7 +149,15 @@ int ConversionStorageSql::MaybeCreateAndStoreConversionReports(
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
const url::Origin& conversion_origin = conversion.conversion_origin();
+ const std::string serialized_conversion_origin =
+ SerializeOrigin(conversion_origin);
+ if (!HasCapacityForStoringConversion(serialized_conversion_origin))
+ return 0;
+
const url::Origin& reporting_origin = conversion.reporting_origin();
+ DCHECK(!conversion_origin.opaque());
+ DCHECK(!reporting_origin.opaque());
+
base::Time current_time = clock_->Now();
int64_t serialized_current_time = SerializeTime(current_time);
@@ -151,7 +172,7 @@ int ConversionStorageSql::MaybeCreateAndStoreConversionReports(
sql::Statement statement(
db_.GetCachedStatement(SQL_FROM_HERE, kGetMatchingImpressionsSql));
- statement.BindString(0, SerializeOrigin(conversion_origin));
+ statement.BindString(0, serialized_conversion_origin);
statement.BindString(1, SerializeOrigin(reporting_origin));
statement.BindInt64(2, serialized_current_time);
@@ -162,6 +183,11 @@ int ConversionStorageSql::MaybeCreateAndStoreConversionReports(
std::string impression_data = statement.ColumnString(1);
url::Origin impression_origin =
DeserializeOrigin(statement.ColumnString(2));
+
+ // Skip the report if the impression origin is opaque. This should only
+ // happen if there is some sort of database corruption.
+ if (impression_origin.opaque())
+ continue;
base::Time impression_time = DeserializeTime(statement.ColumnInt64(3));
base::Time expiry_time = DeserializeTime(statement.ColumnInt64(4));
@@ -274,6 +300,15 @@ std::vector<ConversionReport> ConversionStorageSql::GetConversionsToReport(
base::Time expiry_time = DeserializeTime(statement.ColumnInt64(9));
int64_t impression_id = statement.ColumnInt64(10);
+ // Ensure origins are valid before continuing. This could happen if there is
+ // database corruption.
+ // TODO(csharrison): This should be an extremely rare occurrence but it
+ // would entail that some records will remain in the DB as vestigial if a
+ // conversion is never sent. We should delete these entries from the DB.
+ if (impression_origin.opaque() || conversion_origin.opaque() ||
+ reporting_origin.opaque())
+ continue;
+
// Create the impression and ConversionReport objects from the retrieved
// columns.
StorableImpression impression(impression_data, impression_origin,
@@ -292,6 +327,37 @@ std::vector<ConversionReport> ConversionStorageSql::GetConversionsToReport(
return conversions;
}
+std::vector<StorableImpression> ConversionStorageSql::GetActiveImpressions() {
+ const char kGetImpressionsSql[] =
+ "SELECT impression_data, impression_origin, conversion_origin, "
+ "reporting_origin, impression_time, expiry_time, impression_id "
+ "FROM impressions WHERE active = 1 AND expiry_time > ?";
+ sql::Statement statement(
+ db_.GetCachedStatement(SQL_FROM_HERE, kGetImpressionsSql));
+ statement.BindInt64(0, SerializeTime(clock_->Now()));
+
+ std::vector<StorableImpression> impressions;
+ while (statement.Step()) {
+ std::string impression_data = statement.ColumnString(0);
+ url::Origin impression_origin =
+ DeserializeOrigin(statement.ColumnString(1));
+ url::Origin conversion_origin =
+ DeserializeOrigin(statement.ColumnString(2));
+ url::Origin reporting_origin = DeserializeOrigin(statement.ColumnString(3));
+ base::Time impression_time = DeserializeTime(statement.ColumnInt64(4));
+ base::Time expiry_time = DeserializeTime(statement.ColumnInt64(5));
+ int64_t impression_id = statement.ColumnInt64(6);
+
+ StorableImpression impression(impression_data, impression_origin,
+ conversion_origin, reporting_origin,
+ impression_time, expiry_time, impression_id);
+ impressions.push_back(std::move(impression));
+ }
+ if (!statement.Succeeded())
+ return {};
+ return impressions;
+}
+
int ConversionStorageSql::DeleteExpiredImpressions() {
// Delete all impressions that have no associated conversions and are past
// their expiry time. Optimized by |kImpressionExpiryIndexSql|.
@@ -331,10 +397,205 @@ bool ConversionStorageSql::DeleteConversion(int64_t conversion_id) {
if (!statement.Run())
return false;
- DCHECK_EQ(1, db_.GetLastChangeCount());
return db_.GetLastChangeCount() > 0;
}
+void ConversionStorageSql::ClearData(
+ base::Time delete_begin,
+ base::Time delete_end,
+ base::RepeatingCallback<bool(const url::Origin&)> filter) {
+ SCOPED_UMA_HISTOGRAM_TIMER("Conversions.ClearDataTime");
+ if (filter.is_null()) {
+ ClearAllDataInRange(delete_begin, delete_end);
+ return;
+ }
+
+ // TODO(csharrison, johnidel): This query can be split up and optimized by
+ // adding indexes on the impression_time and conversion_time columns.
+ // See this comment for more information:
+ // crrev.com/c/2150071/4/content/browser/conversions/conversion_storage_sql.cc#342
+ const char kScanCandidateData[] =
+ "SELECT C.conversion_id, I.impression_id,"
+ "I.impression_origin, I.conversion_origin, I.reporting_origin "
+ "FROM impressions I LEFT JOIN conversions C ON "
+ "C.impression_id = I.impression_id WHERE"
+ "(I.impression_time BETWEEN ?1 AND ?2) OR"
+ "(C.conversion_time BETWEEN ?1 AND ?2)";
+ sql::Statement statement(
+ db_.GetCachedStatement(SQL_FROM_HERE, kScanCandidateData));
+ statement.BindInt64(0, SerializeTime(delete_begin));
+ statement.BindInt64(1, SerializeTime(delete_end));
+
+ std::vector<int64_t> impression_ids_to_delete;
+ std::vector<int64_t> conversion_ids_to_delete;
+ while (statement.Step()) {
+ int64_t conversion_id = statement.ColumnInt64(0);
+ int64_t impression_id = statement.ColumnInt64(1);
+ if (filter.Run(DeserializeOrigin(statement.ColumnString(2))) ||
+ filter.Run(DeserializeOrigin(statement.ColumnString(3))) ||
+ filter.Run(DeserializeOrigin(statement.ColumnString(4)))) {
+ impression_ids_to_delete.push_back(impression_id);
+ if (conversion_id != 0)
+ conversion_ids_to_delete.push_back(conversion_id);
+ }
+ }
+
+ // Since multiple conversions can be associated with a single impression,
+ // |impression_ids_to_delete| may contain duplicates. Remove duplicates by
+ // converting the vector into a flat_set. Internally, this sorts the vector
+ // and then removes duplicates.
+ const base::flat_set<int64_t> unique_impression_ids_to_delete(
+ impression_ids_to_delete);
+
+ // TODO(csharrison, johnidel): Should we consider poisoning the DB if some of
+ // the delete operations fail?
+ if (!statement.Succeeded())
+ return;
+
+ // Delete the data in a transaction to avoid cases where the impression part
+ // of a conversion is deleted without deleting the associated conversion, or
+ // vice versa.
+ sql::Transaction transaction(&db_);
+ if (!transaction.Begin())
+ return;
+
+ for (int64_t impression_id : unique_impression_ids_to_delete) {
+ const char kDeleteImpressionSql[] =
+ "DELETE FROM impressions WHERE impression_id = ?";
+ sql::Statement impression_statement(
+ db_.GetCachedStatement(SQL_FROM_HERE, kDeleteImpressionSql));
+ impression_statement.BindInt64(0, impression_id);
+ if (!impression_statement.Run())
+ return;
+ }
+
+ for (int64_t conversion_id : conversion_ids_to_delete) {
+ const char kDeleteConversionSql[] =
+ "DELETE FROM conversions WHERE conversion_id = ?";
+ sql::Statement conversion_statement(
+ db_.GetCachedStatement(SQL_FROM_HERE, kDeleteConversionSql));
+ conversion_statement.BindInt64(0, conversion_id);
+ if (!conversion_statement.Run())
+ return;
+ }
+
+ // Careful! At this point we can still have some vestigial entries in the DB.
+ // For example, if an impression has two conversions, and one conversion is
+ // deleted, the above logic will delete the impression as well, leaving the
+ // second conversion in limbo (it was not in the deletion time range).
+ // Delete all unattributed conversions here to ensure everything is cleaned
+ // up.
+ for (int64_t impression_id : unique_impression_ids_to_delete) {
+ const char kDeleteVestigialConversionSql[] =
+ "DELETE FROM conversions WHERE impression_id = ?";
+ sql::Statement delete_vestigial_statement(
+ db_.GetCachedStatement(SQL_FROM_HERE, kDeleteVestigialConversionSql));
+ delete_vestigial_statement.BindInt64(0, impression_id);
+ if (!delete_vestigial_statement.Run())
+ return;
+ }
+ transaction.Commit();
+}
+
+void ConversionStorageSql::ClearAllDataInRange(base::Time delete_begin,
+ base::Time delete_end) {
+ // Browsing data remover will call this with null |delete_begin|, but also
+ // perform the ClearAllDataAllTime optimization if |delete_begin| is
+ // base::Time::Min().
+ if ((delete_begin.is_null() || delete_begin.is_min()) &&
+ delete_end.is_max()) {
+ ClearAllDataAllTime();
+ return;
+ }
+
+ sql::Transaction transaction(&db_);
+ if (!transaction.Begin())
+ return;
+
+ // Delete all impressions and conversion reports in the given time range.
+ // Note: This should follow the same basic logic in ClearData, with the
+ // assumption that all origins match the filter. This means we can omit a
+ // SELECT statement, and all of the in-memory id management.
+ //
+ // Optimizing these queries are also tough, see this comment for an idea:
+ // http://crrev.com/c/2150071/12/content/browser/conversions/conversion_storage_sql.cc#468
+ const char kDeleteImpressionRangeSql[] =
+ "DELETE FROM impressions WHERE (impression_time BETWEEN ?1 AND ?2) OR "
+ "impression_id in (SELECT impression_id FROM conversions "
+ "WHERE conversion_time BETWEEN ?1 AND ?2)";
+ sql::Statement delete_impressions_statement(
+ db_.GetCachedStatement(SQL_FROM_HERE, kDeleteImpressionRangeSql));
+ delete_impressions_statement.BindInt64(0, SerializeTime(delete_begin));
+ delete_impressions_statement.BindInt64(1, SerializeTime(delete_end));
+ if (!delete_impressions_statement.Run())
+ return;
+
+ const char kDeleteConversionRangeSql[] =
+ "DELETE FROM conversions WHERE (conversion_time BETWEEN ? AND ?) "
+ "OR impression_id NOT IN (SELECT impression_id FROM impressions)";
+ sql::Statement delete_conversions_statement(
+ db_.GetCachedStatement(SQL_FROM_HERE, kDeleteConversionRangeSql));
+ delete_conversions_statement.BindInt64(0, SerializeTime(delete_begin));
+ delete_conversions_statement.BindInt64(1, SerializeTime(delete_end));
+ if (!delete_conversions_statement.Run())
+ return;
+ transaction.Commit();
+}
+
+void ConversionStorageSql::ClearAllDataAllTime() {
+ sql::Transaction transaction(&db_);
+ if (!transaction.Begin())
+ return;
+ const char kDeleteAllConversionsSql[] = "DELETE FROM conversions";
+ const char kDeleteAllImpressionsSql[] = "DELETE FROM impressions";
+ sql::Statement delete_all_conversions_statement(
+ db_.GetCachedStatement(SQL_FROM_HERE, kDeleteAllConversionsSql));
+ sql::Statement delete_all_impressions_statement(
+ db_.GetCachedStatement(SQL_FROM_HERE, kDeleteAllImpressionsSql));
+ if (!delete_all_conversions_statement.Run())
+ return;
+ if (!delete_all_impressions_statement.Run())
+ return;
+ transaction.Commit();
+}
+
+bool ConversionStorageSql::HasCapacityForStoringImpression(
+ const std::string& serialized_origin) {
+ // Optimized by impression_origin_idx.
+ const char kCountImpressionsSql[] =
+ "SELECT COUNT(impression_origin) FROM impressions WHERE "
+ "impression_origin = ?";
+ sql::Statement statement(
+ db_.GetCachedStatement(SQL_FROM_HERE, kCountImpressionsSql));
+ statement.BindString(0, serialized_origin);
+ if (!statement.Step())
+ return false;
+ int64_t count = statement.ColumnInt64(0);
+ return count < delegate_->GetMaxImpressionsPerOrigin();
+}
+
+bool ConversionStorageSql::HasCapacityForStoringConversion(
+ const std::string& serialized_origin) {
+ // This query should be reasonably optimized via conversion_origin_idx. The
+ // conversion origin is the second column in a multi-column index where the
+ // first column is just a boolean. Therefore the second column in the index
+ // should be very well-sorted.
+ //
+ // Note: to take advantage of this, we need to hint to the query planner that
+ // |active| is a boolean, so include it in the conditional.
+ const char kCountConversionsSql[] =
+ "SELECT COUNT(conversion_id) FROM conversions C JOIN impressions I ON"
+ " I.impression_id = C.impression_id"
+ " WHERE I.conversion_origin = ? AND (active BETWEEN 0 AND 1)";
+ sql::Statement statement(
+ db_.GetCachedStatement(SQL_FROM_HERE, kCountConversionsSql));
+ statement.BindString(0, serialized_origin);
+ if (!statement.Step())
+ return false;
+ int64_t count = statement.ColumnInt64(0);
+ return count < delegate_->GetMaxConversionsPerOrigin();
+}
+
bool ConversionStorageSql::InitializeSchema() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(johnidel, csharrison): Many impressions will share a target origin and
@@ -392,6 +653,13 @@ bool ConversionStorageSql::InitializeSchema() {
if (!db_.Execute(kImpressionExpiryIndexSql))
return false;
+ // Optimizes counting impressions by impression origin.
+ const char kImpressionOriginIndexSql[] =
+ "CREATE INDEX IF NOT EXISTS impression_origin_idx "
+ "ON impressions(impression_origin)";
+ if (!db_.Execute(kImpressionOriginIndexSql))
+ 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
diff --git a/chromium/content/browser/conversions/conversion_storage_sql.h b/chromium/content/browser/conversions/conversion_storage_sql.h
index b6d3cbc11d1..686df2f1658 100644
--- a/chromium/content/browser/conversions/conversion_storage_sql.h
+++ b/chromium/content/browser/conversions/conversion_storage_sql.h
@@ -5,6 +5,9 @@
#ifndef CONTENT_BROWSER_CONVERSIONS_CONVERSION_STORAGE_SQL_H_
#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_STORAGE_SQL_H_
+#include <memory>
+#include <vector>
+
#include "base/files/file_path.h"
#include "base/memory/weak_ptr.h"
#include "base/sequence_checker.h"
@@ -26,8 +29,8 @@ namespace content {
class CONTENT_EXPORT ConversionStorageSql : public ConversionStorage {
public:
ConversionStorageSql(const base::FilePath& path_to_database_dir,
- Delegate* delegate,
- base::Clock* clock);
+ std::unique_ptr<Delegate> delegate,
+ const base::Clock* clock);
ConversionStorageSql(const ConversionStorageSql& other) = delete;
ConversionStorageSql& operator=(const ConversionStorageSql& other) = delete;
~ConversionStorageSql() override;
@@ -40,8 +43,20 @@ class CONTENT_EXPORT ConversionStorageSql : public ConversionStorage {
const StorableConversion& conversion) override;
std::vector<ConversionReport> GetConversionsToReport(
base::Time expiry_time) override;
+ std::vector<StorableImpression> GetActiveImpressions() override;
int DeleteExpiredImpressions() override;
bool DeleteConversion(int64_t conversion_id) override;
+ void ClearData(
+ base::Time delete_begin,
+ base::Time delete_end,
+ base::RepeatingCallback<bool(const url::Origin&)> filter) override;
+
+ // Variants of ClearData that assume all Origins match the filter.
+ void ClearAllDataInRange(base::Time delete_begin, base::Time delete_end);
+ void ClearAllDataAllTime();
+
+ bool HasCapacityForStoringImpression(const std::string& serialized_origin);
+ bool HasCapacityForStoringConversion(const std::string& serialized_origin);
bool InitializeSchema();
@@ -51,10 +66,9 @@ class CONTENT_EXPORT ConversionStorageSql : public ConversionStorage {
sql::Database db_;
// Must outlive |this|.
- base::Clock* const clock_;
+ const base::Clock* clock_;
- // Must outlive |this|.
- Delegate* const delegate_;
+ std::unique_ptr<Delegate> delegate_;
SEQUENCE_CHECKER(sequence_checker_);
base::WeakPtrFactory<ConversionStorageSql> weak_factory_;
diff --git a/chromium/content/browser/conversions/conversion_storage_sql_unittest.cc b/chromium/content/browser/conversions/conversion_storage_sql_unittest.cc
index 83179fa46be..ca408b729c3 100644
--- a/chromium/content/browser/conversions/conversion_storage_sql_unittest.cc
+++ b/chromium/content/browser/conversions/conversion_storage_sql_unittest.cc
@@ -4,11 +4,14 @@
#include "content/browser/conversions/conversion_storage_sql.h"
+#include <functional>
#include <memory>
+#include "base/bind.h"
#include "base/files/scoped_temp_dir.h"
#include "base/run_loop.h"
#include "base/test/simple_test_clock.h"
+#include "base/time/time.h"
#include "content/browser/conversions/conversion_report.h"
#include "content/browser/conversions/conversion_test_utils.h"
#include "content/browser/conversions/storable_conversion.h"
@@ -28,8 +31,10 @@ class ConversionStorageSqlTest : public testing::Test {
void OpenDatabase() {
storage_.reset();
- storage_ = std::make_unique<ConversionStorageSql>(temp_directory_.GetPath(),
- &delegate_, &clock_);
+ auto delegate = std::make_unique<ConfigurableStorageDelegate>();
+ delegate_ = delegate.get();
+ storage_ = std::make_unique<ConversionStorageSql>(
+ temp_directory_.GetPath(), std::move(delegate), &clock_);
EXPECT_TRUE(storage_->Initialize());
}
@@ -48,11 +53,13 @@ class ConversionStorageSqlTest : public testing::Test {
ConversionStorage* storage() { return storage_.get(); }
+ ConfigurableStorageDelegate* delegate() { return delegate_; }
+
private:
base::ScopedTempDir temp_directory_;
std::unique_ptr<ConversionStorage> storage_;
+ ConfigurableStorageDelegate* delegate_ = nullptr;
base::SimpleTestClock clock_;
- EmptyStorageDelegate delegate_;
};
TEST_F(ConversionStorageSqlTest,
@@ -66,8 +73,9 @@ TEST_F(ConversionStorageSqlTest,
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));
+ // [impression_origin_idx], [conversion_report_time_idx],
+ // [conversion_impression_id_idx].
+ EXPECT_EQ(5u, sql::test::CountSQLIndices(&raw_db));
}
TEST_F(ConversionStorageSqlTest, DatabaseReopened_DataPersisted) {
@@ -100,4 +108,155 @@ TEST_F(ConversionStorageSqlTest, CorruptDatabase_RecoveredOnOpen) {
EXPECT_TRUE(expecter.SawExpectedErrors());
}
+// Create an impression with two conversions (C1 and C2). Craft a query that
+// will target C2, which will in turn delete the impression. We should ensure
+// that C1 is properly deleted (conversions should not be stored unattributed).
+TEST_F(ConversionStorageSqlTest, ClearDataWithVestigialConversion) {
+ OpenDatabase();
+
+ base::Time start = clock()->Now();
+ auto impression =
+ ImpressionBuilder(start).SetExpiry(base::TimeDelta::FromDays(30)).Build();
+ storage()->StoreImpression(impression);
+
+ clock()->Advance(base::TimeDelta::FromDays(1));
+ EXPECT_EQ(
+ 1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
+
+ clock()->Advance(base::TimeDelta::FromDays(1));
+ EXPECT_EQ(
+ 1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
+
+ // Use a time range that only intersects the last conversion.
+ storage()->ClearData(clock()->Now(), clock()->Now(),
+ base::BindRepeating(std::equal_to<url::Origin>(),
+ impression.impression_origin()));
+ EXPECT_TRUE(storage()->GetConversionsToReport(base::Time::Max()).empty());
+
+ CloseDatabase();
+
+ // Verify that everything is deleted.
+ sql::Database raw_db;
+ EXPECT_TRUE(raw_db.Open(db_path()));
+
+ size_t conversion_rows;
+ size_t impression_rows;
+ sql::test::CountTableRows(&raw_db, "conversions", &conversion_rows);
+ sql::test::CountTableRows(&raw_db, "impressions", &impression_rows);
+
+ EXPECT_EQ(0u, conversion_rows);
+ EXPECT_EQ(0u, impression_rows);
+}
+
+// Same as the above test, but with a null filter.
+TEST_F(ConversionStorageSqlTest, ClearAllDataWithVestigialConversion) {
+ OpenDatabase();
+
+ base::Time start = clock()->Now();
+ auto impression =
+ ImpressionBuilder(start).SetExpiry(base::TimeDelta::FromDays(30)).Build();
+ storage()->StoreImpression(impression);
+
+ clock()->Advance(base::TimeDelta::FromDays(1));
+ EXPECT_EQ(
+ 1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
+
+ clock()->Advance(base::TimeDelta::FromDays(1));
+ EXPECT_EQ(
+ 1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
+
+ // Use a time range that only intersects the last conversion.
+ auto null_filter = base::RepeatingCallback<bool(const url::Origin&)>();
+ storage()->ClearData(clock()->Now(), clock()->Now(), null_filter);
+ EXPECT_TRUE(storage()->GetConversionsToReport(base::Time::Max()).empty());
+
+ CloseDatabase();
+
+ // Verify that everything is deleted.
+ sql::Database raw_db;
+ EXPECT_TRUE(raw_db.Open(db_path()));
+
+ size_t conversion_rows;
+ size_t impression_rows;
+ sql::test::CountTableRows(&raw_db, "conversions", &conversion_rows);
+ sql::test::CountTableRows(&raw_db, "impressions", &impression_rows);
+
+ EXPECT_EQ(0u, conversion_rows);
+ EXPECT_EQ(0u, impression_rows);
+}
+
+// The max time range with a null filter should delete everything.
+TEST_F(ConversionStorageSqlTest, DeleteEverything) {
+ OpenDatabase();
+
+ base::Time start = clock()->Now();
+ for (int i = 0; i < 10; i++) {
+ auto impression = ImpressionBuilder(start)
+ .SetExpiry(base::TimeDelta::FromDays(30))
+ .Build();
+ storage()->StoreImpression(impression);
+ clock()->Advance(base::TimeDelta::FromDays(1));
+ }
+
+ EXPECT_EQ(
+ 10, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
+ clock()->Advance(base::TimeDelta::FromDays(1));
+ EXPECT_EQ(
+ 10, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
+
+ auto null_filter = base::RepeatingCallback<bool(const url::Origin&)>();
+ storage()->ClearData(base::Time::Min(), base::Time::Max(), null_filter);
+ EXPECT_TRUE(storage()->GetConversionsToReport(base::Time::Max()).empty());
+
+ CloseDatabase();
+
+ // Verify that everything is deleted.
+ sql::Database raw_db;
+ EXPECT_TRUE(raw_db.Open(db_path()));
+
+ size_t conversion_rows;
+ size_t impression_rows;
+ sql::test::CountTableRows(&raw_db, "conversions", &conversion_rows);
+ sql::test::CountTableRows(&raw_db, "impressions", &impression_rows);
+
+ EXPECT_EQ(0u, conversion_rows);
+ EXPECT_EQ(0u, impression_rows);
+}
+
+TEST_F(ConversionStorageSqlTest, MaxImpressionsPerOrigin) {
+ OpenDatabase();
+ delegate()->set_max_impressions_per_origin(2);
+ storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
+ storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
+ storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
+ EXPECT_EQ(
+ 2, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
+
+ CloseDatabase();
+ sql::Database raw_db;
+ EXPECT_TRUE(raw_db.Open(db_path()));
+ size_t impression_rows;
+ sql::test::CountTableRows(&raw_db, "impressions", &impression_rows);
+ EXPECT_EQ(2u, impression_rows);
+}
+
+TEST_F(ConversionStorageSqlTest, MaxConversionsPerOrigin) {
+ OpenDatabase();
+ delegate()->set_max_conversions_per_origin(2);
+ storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
+ EXPECT_EQ(
+ 1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
+ EXPECT_EQ(
+ 1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
+ EXPECT_EQ(
+ 0, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
+
+ CloseDatabase();
+ sql::Database raw_db;
+ EXPECT_TRUE(raw_db.Open(db_path()));
+ size_t conversion_rows;
+ sql::test::CountTableRows(&raw_db, "conversions", &conversion_rows);
+ EXPECT_EQ(2u, conversion_rows);
+}
+
} // namespace content
diff --git a/chromium/content/browser/conversions/conversion_storage_unittest.cc b/chromium/content/browser/conversions/conversion_storage_unittest.cc
index c67a69ef253..b8089b96a06 100644
--- a/chromium/content/browser/conversions/conversion_storage_unittest.cc
+++ b/chromium/content/browser/conversions/conversion_storage_unittest.cc
@@ -4,13 +4,17 @@
#include "content/browser/conversions/conversion_storage.h"
+#include <functional>
#include <list>
#include <memory>
#include <tuple>
+#include <utility>
#include <vector>
+#include "base/callback.h"
#include "base/files/scoped_temp_dir.h"
#include "base/strings/string_number_conversions.h"
+#include "base/strings/stringprintf.h"
#include "base/test/simple_test_clock.h"
#include "content/browser/conversions/conversion_report.h"
#include "content/browser/conversions/conversion_storage_sql.h"
@@ -33,45 +37,12 @@ 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;
- ~MockStorageDelegate() override = 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);
- }
+base::RepeatingCallback<bool(const url::Origin&)> GetMatcher(
+ const url::Origin& to_delete) {
+ return base::BindRepeating(std::equal_to<url::Origin>(), to_delete);
+}
- private:
- // List of attribution credits the mock delegate should associate with
- // reports.
- AttributionCredits attribution_credits_;
-};
+} // namespace
// Unit test suite for the ConversionStorage interface. All ConversionStorage
// implementations (including fakes) should be able to re-use this test suite.
@@ -79,8 +50,12 @@ class ConversionStorageTest : public testing::Test {
public:
ConversionStorageTest() {
EXPECT_TRUE(dir_.CreateUniqueTempDir());
- storage_ = std::make_unique<ConversionStorageSql>(dir_.GetPath(),
- &delegate_, &clock_);
+ auto delegate = std::make_unique<ConfigurableStorageDelegate>();
+ delegate->set_report_time_ms(kReportTime);
+ delegate->set_max_conversions_per_impression(kMaxConversions);
+ delegate_ = delegate.get();
+ storage_ = std::make_unique<ConversionStorageSql>(
+ dir_.GetPath(), std::move(delegate), &clock_);
EXPECT_TRUE(storage_->Initialize());
}
@@ -104,20 +79,43 @@ class ConversionStorageTest : public testing::Test {
}
void AddAttributionCredits(AttributionCredits credits) {
- delegate_.AddCredits(credits);
+ delegate_->AddCredits(credits);
}
base::SimpleTestClock* clock() { return &clock_; }
ConversionStorage* storage() { return storage_.get(); }
+ ConfigurableStorageDelegate* delegate() { return delegate_; }
+
private:
- MockStorageDelegate delegate_;
+ ConfigurableStorageDelegate* delegate_;
base::SimpleTestClock clock_;
base::ScopedTempDir dir_;
std::unique_ptr<ConversionStorage> storage_;
};
+TEST_F(ConversionStorageTest, ImpressionStoredAndRetrieved_ValuesIdentical) {
+ auto impression = ImpressionBuilder(clock()->Now()).Build();
+ storage()->StoreImpression(impression);
+ std::vector<StorableImpression> stored_impressions =
+ storage()->GetActiveImpressions();
+ EXPECT_EQ(1u, stored_impressions.size());
+
+ // Verify that each field was stored as expected.
+ EXPECT_EQ(impression.impression_data(),
+ stored_impressions[0].impression_data());
+ EXPECT_EQ(impression.impression_origin(),
+ stored_impressions[0].impression_origin());
+ EXPECT_EQ(impression.conversion_origin(),
+ stored_impressions[0].conversion_origin());
+ EXPECT_EQ(impression.reporting_origin(),
+ stored_impressions[0].reporting_origin());
+ EXPECT_EQ(impression.impression_time(),
+ stored_impressions[0].impression_time());
+ EXPECT_EQ(impression.expiry_time(), stored_impressions[0].expiry_time());
+}
+
TEST_F(ConversionStorageTest,
GetWithNoMatchingImpressions_NoImpressionsReturned) {
EXPECT_EQ(
@@ -547,4 +545,279 @@ TEST_F(ConversionStorageTest,
EXPECT_TRUE(ReportsEqual(expected_reports, actual_reports));
}
+TEST_F(ConversionStorageTest, MaxImpressionsPerOrigin) {
+ delegate()->set_max_impressions_per_origin(2);
+ storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
+ storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
+ storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
+ EXPECT_EQ(
+ 2, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
+}
+
+TEST_F(ConversionStorageTest, MaxConversionsPerOrigin) {
+ delegate()->set_max_conversions_per_origin(2);
+ storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
+ EXPECT_EQ(
+ 1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
+ EXPECT_EQ(
+ 1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
+ EXPECT_EQ(
+ 0, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
+}
+
+TEST_F(ConversionStorageTest, ClearDataWithNoMatch_NoDelete) {
+ base::Time now = clock()->Now();
+ auto impression = ImpressionBuilder(now).Build();
+ storage()->StoreImpression(impression);
+ storage()->ClearData(
+ now, now, GetMatcher(url::Origin::Create(GURL("https://no-match.com"))));
+ EXPECT_EQ(
+ 1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
+}
+
+TEST_F(ConversionStorageTest, ClearDataOutsideRange_NoDelete) {
+ base::Time now = clock()->Now();
+ auto impression = ImpressionBuilder(now).Build();
+ storage()->StoreImpression(impression);
+
+ storage()->ClearData(now + base::TimeDelta::FromMinutes(10),
+ now + base::TimeDelta::FromMinutes(20),
+ GetMatcher(impression.impression_origin()));
+ EXPECT_EQ(
+ 1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
+}
+
+TEST_F(ConversionStorageTest, ClearDataImpression) {
+ base::Time now = clock()->Now();
+ {
+ auto impression = ImpressionBuilder(now).Build();
+ storage()->StoreImpression(impression);
+ storage()->ClearData(now, now + base::TimeDelta::FromMinutes(20),
+ GetMatcher(impression.impression_origin()));
+ EXPECT_EQ(0, storage()->MaybeCreateAndStoreConversionReports(
+ DefaultConversion()));
+ }
+ {
+ auto impression = ImpressionBuilder(now).Build();
+ storage()->StoreImpression(impression);
+ storage()->ClearData(now, now + base::TimeDelta::FromMinutes(20),
+ GetMatcher(impression.reporting_origin()));
+ EXPECT_EQ(0, storage()->MaybeCreateAndStoreConversionReports(
+ DefaultConversion()));
+ }
+ {
+ auto impression = ImpressionBuilder(now).Build();
+ storage()->StoreImpression(impression);
+ storage()->ClearData(now, now + base::TimeDelta::FromMinutes(20),
+ GetMatcher(impression.conversion_origin()));
+ EXPECT_EQ(0, storage()->MaybeCreateAndStoreConversionReports(
+ DefaultConversion()));
+ }
+}
+
+TEST_F(ConversionStorageTest, ClearDataImpressionConversion) {
+ base::Time now = clock()->Now();
+ auto impression = ImpressionBuilder(now).Build();
+ auto conversion = DefaultConversion();
+
+ storage()->StoreImpression(impression);
+ EXPECT_EQ(1, storage()->MaybeCreateAndStoreConversionReports(conversion));
+
+ storage()->ClearData(now - base::TimeDelta::FromMinutes(20),
+ now + base::TimeDelta::FromMinutes(20),
+ GetMatcher(impression.impression_origin()));
+
+ EXPECT_TRUE(storage()->GetConversionsToReport(base::Time::Max()).empty());
+}
+
+// The null filter should match all origins.
+TEST_F(ConversionStorageTest, ClearDataNullFilter) {
+ base::Time now = clock()->Now();
+
+ for (int i = 0; i < 10; i++) {
+ auto origin =
+ url::Origin::Create(GURL(base::StringPrintf("https://%d.com/", i)));
+ storage()->StoreImpression(ImpressionBuilder(now)
+ .SetExpiry(base::TimeDelta::FromDays(30))
+ .SetImpressionOrigin(origin)
+ .SetReportingOrigin(origin)
+ .SetConversionOrigin(origin)
+ .Build());
+ clock()->Advance(base::TimeDelta::FromDays(1));
+ }
+
+ // Convert half of them now, half after another day.
+ for (int i = 0; i < 5; i++) {
+ auto origin =
+ url::Origin::Create(GURL(base::StringPrintf("https://%d.com/", i)));
+ StorableConversion conversion("1", origin, origin);
+ EXPECT_EQ(1, storage()->MaybeCreateAndStoreConversionReports(conversion));
+ }
+ clock()->Advance(base::TimeDelta::FromDays(1));
+ for (int i = 5; i < 10; i++) {
+ auto origin =
+ url::Origin::Create(GURL(base::StringPrintf("https://%d.com/", i)));
+ StorableConversion conversion("1", origin, origin);
+ EXPECT_EQ(1, storage()->MaybeCreateAndStoreConversionReports(conversion));
+ }
+
+ auto null_filter = base::RepeatingCallback<bool(const url::Origin&)>();
+ storage()->ClearData(clock()->Now(), clock()->Now(), null_filter);
+ EXPECT_EQ(5u, storage()->GetConversionsToReport(base::Time::Max()).size());
+}
+
+TEST_F(ConversionStorageTest, ClearDataWithImpressionOutsideRange) {
+ base::Time start = clock()->Now();
+ auto impression =
+ ImpressionBuilder(start).SetExpiry(base::TimeDelta::FromDays(30)).Build();
+ auto conversion = DefaultConversion();
+
+ storage()->StoreImpression(impression);
+
+ EXPECT_EQ(1, storage()->MaybeCreateAndStoreConversionReports(conversion));
+ storage()->ClearData(clock()->Now(), clock()->Now(),
+ GetMatcher(impression.impression_origin()));
+ EXPECT_TRUE(storage()->GetConversionsToReport(base::Time::Max()).empty());
+}
+
+// Deletions with time range between the impression and conversion should not
+// delete anything, unless the time range intersects one of the events.
+TEST_F(ConversionStorageTest, ClearDataRangeBetweenEvents) {
+ base::Time start = clock()->Now();
+ auto impression =
+ ImpressionBuilder(start).SetExpiry(base::TimeDelta::FromDays(30)).Build();
+ auto conversion = DefaultConversion();
+
+ std::vector<ConversionReport> expected_reports = {
+ GetExpectedReport(impression, conversion, 0)};
+
+ storage()->StoreImpression(impression);
+
+ clock()->Advance(base::TimeDelta::FromDays(1));
+
+ EXPECT_EQ(1, storage()->MaybeCreateAndStoreConversionReports(conversion));
+
+ storage()->ClearData(start + base::TimeDelta::FromMinutes(1),
+ start + base::TimeDelta::FromMinutes(10),
+ GetMatcher(impression.impression_origin()));
+
+ std::vector<ConversionReport> actual_reports =
+ storage()->GetConversionsToReport(base::Time::Max());
+ EXPECT_TRUE(ReportsEqual(expected_reports, actual_reports));
+}
+// Test that only a subset of impressions / conversions are deleted with
+// multiple impressions per conversion, if only a subset of impressions match.
+TEST_F(ConversionStorageTest, ClearDataWithMultiTouch) {
+ base::Time start = clock()->Now();
+ auto impression1 =
+ ImpressionBuilder(start).SetExpiry(base::TimeDelta::FromDays(30)).Build();
+ storage()->StoreImpression(impression1);
+
+ clock()->Advance(base::TimeDelta::FromDays(1));
+ auto impression2 = ImpressionBuilder(clock()->Now())
+ .SetExpiry(base::TimeDelta::FromDays(30))
+ .Build();
+ auto impression3 = ImpressionBuilder(clock()->Now())
+ .SetExpiry(base::TimeDelta::FromDays(30))
+ .Build();
+
+ storage()->StoreImpression(impression2);
+ storage()->StoreImpression(impression3);
+
+ EXPECT_EQ(
+ 3, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
+
+ // Only the first impression should overlap with this time range, but all the
+ // impressions should share the origin.
+ storage()->ClearData(start, start,
+ GetMatcher(impression1.impression_origin()));
+ EXPECT_EQ(2u, storage()->GetConversionsToReport(base::Time::Max()).size());
+}
+
+// Attribution occurs at conversion time, not report time, so deleted
+// impressions should not adjust credit allocation.
+TEST_F(ConversionStorageTest, ClearData_AttributionUnaffected) {
+ auto impression1 = ImpressionBuilder(clock()->Now())
+ .SetData("xyz")
+ .SetExpiry(base::TimeDelta::FromDays(30))
+ .Build();
+ auto impression2 = ImpressionBuilder(clock()->Now())
+ .SetData("abc")
+ .SetExpiry(base::TimeDelta::FromDays(30))
+ .Build();
+ auto conversion = DefaultConversion();
+ storage()->StoreImpression(impression1);
+ storage()->StoreImpression(impression2);
+ std::vector<ConversionReport> expected_reports = {
+ GetExpectedReport(impression1, conversion, 0),
+ GetExpectedReport(impression2, conversion, 0)};
+
+ clock()->Advance(base::TimeDelta::FromDays(1));
+ auto impression3 = ImpressionBuilder(clock()->Now())
+ .SetExpiry(base::TimeDelta::FromDays(30))
+ .Build();
+ storage()->StoreImpression(impression3);
+ base::Time delete_time = clock()->Now();
+ clock()->Advance(base::TimeDelta::FromDays(1));
+
+ AddAttributionCredits({100, 0, 0});
+ EXPECT_EQ(3, storage()->MaybeCreateAndStoreConversionReports(conversion));
+
+ // The last impression should be deleted, but the conversion shouldn't be.
+ storage()->ClearData(delete_time, delete_time,
+ GetMatcher(impression1.impression_origin()));
+ std::vector<ConversionReport> actual_reports =
+ storage()->GetConversionsToReport(base::Time::Max());
+ EXPECT_TRUE(ReportsEqual(expected_reports, actual_reports));
+}
+
+// The max time range with a null filter should delete everything.
+TEST_F(ConversionStorageTest, DeleteAll) {
+ base::Time start = clock()->Now();
+ for (int i = 0; i < 10; i++) {
+ auto impression = ImpressionBuilder(start)
+ .SetExpiry(base::TimeDelta::FromDays(30))
+ .Build();
+ storage()->StoreImpression(impression);
+ clock()->Advance(base::TimeDelta::FromDays(1));
+ }
+
+ EXPECT_EQ(
+ 10, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
+ clock()->Advance(base::TimeDelta::FromDays(1));
+ EXPECT_EQ(
+ 10, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
+
+ auto null_filter = base::RepeatingCallback<bool(const url::Origin&)>();
+ storage()->ClearData(base::Time::Min(), base::Time::Max(), null_filter);
+
+ // Verify that everything is deleted.
+ EXPECT_TRUE(storage()->GetConversionsToReport(base::Time::Max()).empty());
+}
+
+// Same as the above test, but uses base::Time() instead of base::Time::Min()
+// for delete_begin, which should yield the same behavior.
+TEST_F(ConversionStorageTest, DeleteAllNullDeleteBegin) {
+ base::Time start = clock()->Now();
+ for (int i = 0; i < 10; i++) {
+ auto impression = ImpressionBuilder(start)
+ .SetExpiry(base::TimeDelta::FromDays(30))
+ .Build();
+ storage()->StoreImpression(impression);
+ clock()->Advance(base::TimeDelta::FromDays(1));
+ }
+
+ EXPECT_EQ(
+ 10, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
+ clock()->Advance(base::TimeDelta::FromDays(1));
+ EXPECT_EQ(
+ 10, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
+
+ auto null_filter = base::RepeatingCallback<bool(const url::Origin&)>();
+ storage()->ClearData(base::Time(), base::Time::Max(), null_filter);
+
+ // Verify that everything is deleted.
+ EXPECT_TRUE(storage()->GetConversionsToReport(base::Time::Max()).empty());
+}
+
} // namespace content
diff --git a/chromium/content/browser/conversions/conversion_test_utils.cc b/chromium/content/browser/conversions/conversion_test_utils.cc
index 7209814154c..10f95f365bf 100644
--- a/chromium/content/browser/conversions/conversion_test_utils.cc
+++ b/chromium/content/browser/conversions/conversion_test_utils.cc
@@ -4,25 +4,121 @@
#include "content/browser/conversions/conversion_test_utils.h"
+#include <limits.h>
+
#include <tuple>
+#include "base/bind.h"
+#include "base/bind_helpers.h"
+#include "base/callback.h"
+#include "base/run_loop.h"
+#include "base/task_runner_util.h"
+#include "base/test/bind_test_util.h"
#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/";
+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;
+ConfigurableStorageDelegate::ConfigurableStorageDelegate() = default;
+ConfigurableStorageDelegate::~ConfigurableStorageDelegate() = default;
+
+void ConfigurableStorageDelegate::ProcessNewConversionReports(
+ std::vector<ConversionReport>* reports) {
+ // Note: reports are ordered by impression time, descending.
+ for (auto& report : *reports) {
+ report.report_time = report.impression.impression_time() +
+ base::TimeDelta::FromMilliseconds(report_time_ms_);
+
+ // 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 ConfigurableStorageDelegate::GetMaxConversionsPerImpression() const {
+ return max_conversions_per_impression_;
+}
+int ConfigurableStorageDelegate::GetMaxImpressionsPerOrigin() const {
+ return max_impressions_per_origin_;
+}
+int ConfigurableStorageDelegate::GetMaxConversionsPerOrigin() const {
+ return max_conversions_per_origin_;
+}
+
+ConversionManager* TestManagerProvider::GetManager(
+ WebContents* web_contents) const {
+ return manager_;
+}
+
+TestConversionManager::TestConversionManager() = default;
+
+TestConversionManager::~TestConversionManager() = default;
+
+void TestConversionManager::HandleImpression(
+ const StorableImpression& impression) {
+ num_impressions_++;
+}
+
+void TestConversionManager::HandleConversion(
+ const StorableConversion& conversion) {
+ num_conversions_++;
+}
+
+void TestConversionManager::GetActiveImpressionsForWebUI(
+ base::OnceCallback<void(std::vector<StorableImpression>)> callback) {
+ std::move(callback).Run(impressions_);
+}
+
+void TestConversionManager::GetReportsForWebUI(
+ base::OnceCallback<void(std::vector<ConversionReport>)> callback,
+ base::Time max_report_time) {
+ std::move(callback).Run(reports_);
+}
+
+void TestConversionManager::SendReportsForWebUI(base::OnceClosure done) {
+ reports_.clear();
+ std::move(done).Run();
+}
+
+const ConversionPolicy& TestConversionManager::GetConversionPolicy() const {
+ return policy_;
+}
+
+void TestConversionManager::ClearData(
+ base::Time delete_begin,
+ base::Time delete_end,
+ base::RepeatingCallback<bool(const url::Origin&)> filter,
+ base::OnceClosure done) {
+ impressions_.clear();
+ reports_.clear();
+ std::move(done).Run();
+}
+
+void TestConversionManager::SetActiveImpressionsForWebUI(
+ std::vector<StorableImpression> impressions) {
+ impressions_ = std::move(impressions);
+}
+
+void TestConversionManager::SetReportsForWebUI(
+ std::vector<ConversionReport> reports) {
+ reports_ = std::move(reports);
+}
+
+void TestConversionManager::Reset() {
+ num_impressions_ = 0u;
+ num_conversions_ = 0u;
}
// Builds an impression with default values. This is done as a builder because
@@ -82,6 +178,23 @@ StorableConversion DefaultConversion() {
return conversion;
}
+// Custom comparator for StorableImpressions that does not take impression id's
+// into account.
+testing::AssertionResult ImpressionsEqual(const StorableImpression& expected,
+ const StorableImpression& actual) {
+ const auto tie = [](const StorableImpression& impression) {
+ return std::make_tuple(
+ impression.impression_data(), impression.impression_origin(),
+ impression.conversion_origin(), impression.reporting_origin(),
+ impression.impression_time(), impression.expiry_time());
+ };
+
+ if (tie(expected) != tie(actual)) {
+ return testing::AssertionFailure();
+ }
+ return testing::AssertionSuccess();
+}
+
// 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.
@@ -114,4 +227,23 @@ testing::AssertionResult ReportsEqual(
return testing::AssertionSuccess();
}
+std::vector<ConversionReport> GetConversionsToReportForTesting(
+ ConversionManagerImpl* manager,
+ base::Time max_report_time) {
+ base::RunLoop run_loop;
+ std::vector<ConversionReport> conversion_reports;
+ base::PostTaskAndReplyWithResult(
+ manager->storage_task_runner_.get(), FROM_HERE,
+ base::BindOnce(&ConversionStorage::GetConversionsToReport,
+ base::Unretained(manager->storage_.get()),
+ max_report_time),
+ base::BindOnce(base::BindLambdaForTesting(
+ [&](std::vector<ConversionReport> reports) {
+ conversion_reports = std::move(reports);
+ run_loop.Quit();
+ })));
+ run_loop.Run();
+ return conversion_reports;
+}
+
} // namespace content
diff --git a/chromium/content/browser/conversions/conversion_test_utils.h b/chromium/content/browser/conversions/conversion_test_utils.h
index 20b0c378cfd..80e569b5cfb 100644
--- a/chromium/content/browser/conversions/conversion_test_utils.h
+++ b/chromium/content/browser/conversions/conversion_test_utils.h
@@ -5,10 +5,15 @@
#ifndef CONTENT_BROWSER_CONVERSIONS_CONVERSION_TEST_UTILS_H_
#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_TEST_UTILS_H_
+#include <list>
#include <string>
#include <vector>
+#include "base/memory/scoped_refptr.h"
+#include "base/sequenced_task_runner.h"
#include "base/time/time.h"
+#include "content/browser/conversions/conversion_manager.h"
+#include "content/browser/conversions/conversion_manager_impl.h"
#include "content/browser/conversions/conversion_report.h"
#include "content/browser/conversions/conversion_storage.h"
#include "content/browser/conversions/storable_conversion.h"
@@ -18,16 +23,105 @@
namespace content {
-class EmptyStorageDelegate : public ConversionStorage::Delegate {
+class ConfigurableStorageDelegate : public ConversionStorage::Delegate {
public:
- EmptyStorageDelegate() = default;
- ~EmptyStorageDelegate() override = default;
+ using AttributionCredits = std::list<int>;
+ ConfigurableStorageDelegate();
+ ~ConfigurableStorageDelegate() override;
// ConversionStorage::Delegate
void ProcessNewConversionReports(
- std::vector<ConversionReport>* reports) override {}
-
+ std::vector<ConversionReport>* reports) override;
int GetMaxConversionsPerImpression() const override;
+ int GetMaxImpressionsPerOrigin() const override;
+ int GetMaxConversionsPerOrigin() const override;
+
+ void set_max_conversions_per_impression(int max) {
+ max_conversions_per_impression_ = max;
+ }
+
+ void set_max_impressions_per_origin(int max) {
+ max_impressions_per_origin_ = max;
+ }
+
+ void set_max_conversions_per_origin(int max) {
+ max_conversions_per_origin_ = max;
+ }
+
+ void set_report_time_ms(int report_time_ms) {
+ report_time_ms_ = report_time_ms;
+ }
+
+ void AddCredits(AttributionCredits credits) {
+ // Add all credits to our list in order.
+ attribution_credits_.splice(attribution_credits_.end(), credits);
+ }
+
+ private:
+ int max_conversions_per_impression_ = INT_MAX;
+ int max_impressions_per_origin_ = INT_MAX;
+ int max_conversions_per_origin_ = INT_MAX;
+
+ int report_time_ms_ = 0;
+
+ // List of attribution credits the test delegate should associate with
+ // reports.
+ AttributionCredits attribution_credits_;
+};
+
+// Test manager provider which can be used to inject a fake ConversionManager.
+class TestManagerProvider : public ConversionManager::Provider {
+ public:
+ explicit TestManagerProvider(ConversionManager* manager)
+ : manager_(manager) {}
+ ~TestManagerProvider() override = default;
+
+ ConversionManager* GetManager(WebContents* web_contents) const override;
+
+ private:
+ ConversionManager* manager_ = nullptr;
+};
+
+// Test ConversionManager which can be injected into tests to monitor calls to a
+// ConversionManager instance.
+class TestConversionManager : public ConversionManager {
+ public:
+ TestConversionManager();
+ ~TestConversionManager() override;
+
+ // ConversionManager:
+ void HandleImpression(const StorableImpression& impression) override;
+ void HandleConversion(const StorableConversion& conversion) override;
+ void GetActiveImpressionsForWebUI(
+ base::OnceCallback<void(std::vector<StorableImpression>)> callback)
+ override;
+ void GetReportsForWebUI(
+ base::OnceCallback<void(std::vector<ConversionReport>)> callback,
+ base::Time max_report_time) override;
+ void SendReportsForWebUI(base::OnceClosure done) override;
+ const ConversionPolicy& GetConversionPolicy() const override;
+ void ClearData(base::Time delete_begin,
+ base::Time delete_end,
+ base::RepeatingCallback<bool(const url::Origin&)> filter,
+ base::OnceClosure done) override;
+
+ void SetActiveImpressionsForWebUI(
+ std::vector<StorableImpression> impressions);
+ void SetReportsForWebUI(std::vector<ConversionReport> reports);
+
+ // Resets all counters on this.
+ void Reset();
+
+ size_t num_impressions() const { return num_impressions_; }
+ size_t num_conversions() const { return num_conversions_; }
+
+ private:
+ ConversionPolicy policy_;
+ size_t num_impressions_ = 0;
+ size_t num_conversions_ = 0;
+
+ std::vector<StorableImpression> impressions_;
+ std::vector<ConversionReport> reports_;
};
// Helper class to construct a StorableImpression for tests using default data.
@@ -35,7 +129,7 @@ class EmptyStorageDelegate : public ConversionStorage::Delegate {
// builder pattern.
class ImpressionBuilder {
public:
- ImpressionBuilder(base::Time time);
+ explicit ImpressionBuilder(base::Time time);
~ImpressionBuilder();
ImpressionBuilder& SetExpiry(base::TimeDelta delta);
@@ -63,10 +157,17 @@ class ImpressionBuilder {
// impressions created by ImpressionBuilder.
StorableConversion DefaultConversion();
+testing::AssertionResult ImpressionsEqual(const StorableImpression& expected,
+ const StorableImpression& actual);
+
testing::AssertionResult ReportsEqual(
const std::vector<ConversionReport>& expected,
const std::vector<ConversionReport>& actual);
+std::vector<ConversionReport> GetConversionsToReportForTesting(
+ ConversionManagerImpl* manager,
+ base::Time max_report_time);
+
} // namespace content
#endif // CONTENT_BROWSER_CONVERSIONS_CONVERSION_TEST_UTILS_H_
diff --git a/chromium/content/browser/conversions/conversions_browsertest.cc b/chromium/content/browser/conversions/conversions_browsertest.cc
new file mode 100644
index 00000000000..dedcf5a42ec
--- /dev/null
+++ b/chromium/content/browser/conversions/conversions_browsertest.cc
@@ -0,0 +1,254 @@
+// 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 <memory>
+
+#include "base/command_line.h"
+#include "base/test/scoped_feature_list.h"
+#include "content/public/common/content_features.h"
+#include "content/public/common/content_switches.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "content/public/test/content_browser_test.h"
+#include "content/public/test/content_browser_test_utils.h"
+#include "content/public/test/test_navigation_observer.h"
+#include "content/shell/browser/shell.h"
+#include "net/dns/mock_host_resolver.h"
+#include "net/test/embedded_test_server/controllable_http_response.h"
+#include "net/test/embedded_test_server/default_handlers.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "net/test/embedded_test_server/http_request.h"
+#include "net/test/embedded_test_server/http_response.h"
+#include "net/test/embedded_test_server/request_handler_util.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace content {
+
+namespace {
+
+// Waits for the a given |report_url| to be received by the test server. Wraps a
+// ControllableHttpResponse so that it can wait for the server request in a
+// thread-safe manner. Therefore, these must be registered prior to |server|
+// starting.
+struct ExpectedReportWaiter {
+ // ControllableHTTPResponses can only wait for relative urls, so only supply
+ // the path + query.
+ ExpectedReportWaiter(const GURL& report_url, net::EmbeddedTestServer* server)
+ : expected_url(report_url),
+ response(std::make_unique<net::test_server::ControllableHttpResponse>(
+ server,
+ report_url.path() + "?" + report_url.query())) {}
+
+ GURL expected_url;
+ std::unique_ptr<net::test_server::ControllableHttpResponse> response;
+
+ // Returns the url for the HttpRequest handled by |response|. This returns a
+ // URL formatted with the host defined in the headers. This would not match
+ // |expected_url| if the host for report url was not set properly.
+ GURL WaitForRequestUrl() {
+ if (!response->http_request())
+ response->WaitForRequest();
+
+ // The embedded test server resolves all urls to 127.0.0.1, so get the real
+ // request host from the request headers.
+ const net::test_server::HttpRequest& request = *response->http_request();
+ DCHECK(request.headers.find("Host") != request.headers.end());
+ const GURL& request_url = request.GetURL();
+ GURL header_url = GURL("https://" + request.headers.at("Host"));
+ std::string host = header_url.host();
+ GURL::Replacements replace_host;
+ replace_host.SetHostStr(host);
+
+ // Clear the port as it is assigned by the EmbeddedTestServer at runtime.
+ replace_host.SetPortStr("");
+ return request_url.ReplaceComponents(replace_host);
+ }
+};
+
+} // namespace
+
+class ConversionsBrowserTest : public ContentBrowserTest {
+ public:
+ ConversionsBrowserTest() {
+ feature_list_.InitAndEnableFeature(features::kConversionMeasurement);
+ }
+
+ void SetUpCommandLine(base::CommandLine* command_line) override {
+ command_line->AppendSwitch(switches::kConversionsDebugMode);
+ }
+
+ void SetUpOnMainThread() override {
+ host_resolver()->AddRule("*", "127.0.0.1");
+
+ https_server_ = std::make_unique<net::EmbeddedTestServer>(
+ net::EmbeddedTestServer::TYPE_HTTPS);
+ https_server_->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
+ net::test_server::RegisterDefaultHandlers(https_server_.get());
+ https_server_->ServeFilesFromSourceDirectory("content/test/data");
+ SetupCrossSiteRedirector(https_server_.get());
+ }
+
+ WebContents* web_contents() { return shell()->web_contents(); }
+
+ net::EmbeddedTestServer* https_server() { return https_server_.get(); }
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+ std::unique_ptr<net::EmbeddedTestServer> https_server_;
+};
+
+IN_PROC_BROWSER_TEST_F(ConversionsBrowserTest,
+ ImpressionConversion_ReportSent) {
+ // Expected reports must be registered before the server starts.
+ ExpectedReportWaiter expected_report(
+ GURL(
+ "https://a.test/.well-known/"
+ "register-conversion?impression-data=1&conversion-data=7&credit=100"),
+ https_server());
+ ASSERT_TRUE(https_server()->Start());
+
+ GURL impression_url = https_server()->GetURL(
+ "a.test", "/conversions/page_with_impression_creator.html");
+ EXPECT_TRUE(NavigateToURL(web_contents(), impression_url));
+
+ // Create an anchor tag with impression attributes and click the link. By
+ // default the target is set to "_top".
+ GURL conversion_url = https_server()->GetURL(
+ "b.test", "/conversions/page_with_conversion_redirect.html");
+ EXPECT_TRUE(
+ ExecJs(web_contents(),
+ JsReplace(R"(
+ createImpressionTag("link" /* id */,
+ $1 /* url */,
+ "1" /* impression data */,
+ $2 /* conversion_destination */);)",
+ conversion_url, url::Origin::Create(conversion_url))));
+
+ TestNavigationObserver observer(web_contents());
+ EXPECT_TRUE(ExecJs(shell(), "simulateClick('link');"));
+ observer.Wait();
+
+ // Register a conversion with the original page as the reporting origin.
+ EXPECT_TRUE(
+ ExecJs(web_contents(), JsReplace("registerConversionForOrigin(7, $1)",
+ url::Origin::Create(impression_url))));
+
+ EXPECT_EQ(expected_report.expected_url, expected_report.WaitForRequestUrl());
+}
+
+IN_PROC_BROWSER_TEST_F(ConversionsBrowserTest,
+ ImpressionFromCrossOriginSubframe_ReportSent) {
+ ExpectedReportWaiter expected_report(
+ GURL(
+ "https://a.test/.well-known/"
+ "register-conversion?impression-data=1&conversion-data=7&credit=100"),
+ https_server());
+ ASSERT_TRUE(https_server()->Start());
+
+ GURL page_url = https_server()->GetURL("a.test", "/page_with_iframe.html");
+ EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
+
+ GURL subframe_url = https_server()->GetURL(
+ "c.test", "/conversions/page_with_impression_creator.html");
+ EXPECT_TRUE(ExecJs(shell(), R"(
+ let frame= document.getElementById('test_iframe');
+ frame.setAttribute('allow', 'conversion-measurement');)"));
+ NavigateIframeToURL(web_contents(), "test_iframe", subframe_url);
+ RenderFrameHost* subframe = ChildFrameAt(web_contents()->GetMainFrame(), 0);
+
+ // Create an impression tag in the subframe and target a popup window.
+ GURL conversion_url = https_server()->GetURL(
+ "b.test", "/conversions/page_with_conversion_redirect.html");
+ EXPECT_TRUE(ExecJs(subframe, JsReplace(R"(
+ createImpressionTagWithTarget("link" /* id */,
+ $1 /* url */,
+ "1" /* impression data */,
+ $2 /* conversion_destination */,
+ "new_frame" /* target */);)",
+ conversion_url,
+ url::Origin::Create(conversion_url))));
+
+ ShellAddedObserver new_shell_observer;
+ TestNavigationObserver observer(nullptr);
+ observer.StartWatchingNewWebContents();
+ EXPECT_TRUE(ExecJs(subframe, "simulateClick('link');"));
+ WebContents* popup_contents = new_shell_observer.GetShell()->web_contents();
+ observer.Wait();
+
+ // Register a conversion with the original page as the reporting origin.
+ EXPECT_TRUE(
+ ExecJs(popup_contents, JsReplace("registerConversionForOrigin(7, $1)",
+ url::Origin::Create(page_url))));
+
+ EXPECT_EQ(expected_report.expected_url, expected_report.WaitForRequestUrl());
+}
+
+IN_PROC_BROWSER_TEST_F(
+ ConversionsBrowserTest,
+ MultipleImpressionsPerConversion_ReportsSentWithAttribution) {
+ std::vector<ExpectedReportWaiter> expected_reports;
+ expected_reports.emplace_back(
+ GURL("https://d.test/.well-known/"
+ "register-conversion?impression-data=1&conversion-data=7&credit=0"),
+ https_server());
+ expected_reports.emplace_back(
+ GURL(
+ "https://d.test/.well-known/"
+ "register-conversion?impression-data=2&conversion-data=7&credit=100"),
+ https_server());
+ ASSERT_TRUE(https_server()->Start());
+
+ GURL first_impression_url = https_server()->GetURL(
+ "a.test", "/conversions/page_with_impression_creator.html");
+ EXPECT_TRUE(NavigateToURL(web_contents(), first_impression_url));
+
+ GURL second_impression_url = https_server()->GetURL(
+ "c.test", "/conversions/page_with_impression_creator.html");
+ Shell* shell2 =
+ Shell::CreateNewWindow(shell()->web_contents()->GetBrowserContext(),
+ GURL(), nullptr, gfx::Size(100, 100));
+ EXPECT_TRUE(NavigateToURL(shell2->web_contents(), second_impression_url));
+
+ // Register impressions from both windows.
+ GURL conversion_url = https_server()->GetURL(
+ "b.test", "/conversions/page_with_conversion_redirect.html");
+ url::Origin reporting_origin =
+ url::Origin::Create(https_server()->GetURL("d.test", "/"));
+ std::string impression_js = R"(
+ createImpressionTagWithReporting("link" /* id */,
+ $1 /* url */,
+ $2 /* impression data */,
+ $3 /* conversion_destination */,
+ $4 /* reporting_origin */);)";
+
+ TestNavigationObserver first_nav_observer(shell()->web_contents());
+ EXPECT_TRUE(
+ ExecJs(shell(),
+ JsReplace(impression_js, conversion_url, "1" /* impression_data */,
+ url::Origin::Create(conversion_url), reporting_origin)));
+ EXPECT_TRUE(ExecJs(shell(), "simulateClick('link');"));
+ first_nav_observer.Wait();
+
+ TestNavigationObserver second_nav_observer(shell2->web_contents());
+ EXPECT_TRUE(
+ ExecJs(shell2,
+ JsReplace(impression_js, conversion_url, "2" /* impression_data */,
+ url::Origin::Create(conversion_url), reporting_origin)));
+ EXPECT_TRUE(ExecJs(shell2, "simulateClick('link');"));
+ second_nav_observer.Wait();
+
+ // Register a conversion after both impressions have been registered.
+ EXPECT_TRUE(ExecJs(shell2, JsReplace("registerConversionForOrigin(7, $1)",
+ reporting_origin)));
+
+ for (auto& report : expected_reports) {
+ if (!report.response->http_request())
+ report.response->WaitForRequest();
+ EXPECT_EQ(report.expected_url, report.WaitForRequestUrl());
+ }
+}
+
+} // namespace content
diff --git a/chromium/content/browser/conversions/impression_declaration_browsertest.cc b/chromium/content/browser/conversions/impression_declaration_browsertest.cc
new file mode 100644
index 00000000000..9e12221ca75
--- /dev/null
+++ b/chromium/content/browser/conversions/impression_declaration_browsertest.cc
@@ -0,0 +1,478 @@
+// 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 <stdint.h>
+#include <memory>
+
+#include "base/bind.h"
+#include "base/run_loop.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/time/time.h"
+#include "content/browser/web_contents/web_contents_impl.h"
+#include "content/public/browser/navigation_handle.h"
+#include "content/public/common/content_features.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "content/public/test/content_browser_test.h"
+#include "content/public/test/content_browser_test_utils.h"
+#include "content/shell/browser/shell.h"
+#include "net/dns/mock_host_resolver.h"
+#include "net/test/embedded_test_server/default_handlers.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "url/gurl.h"
+
+namespace content {
+
+// WebContentsObserver that waits until an impression is available on a
+// navigation handle for a finished navigation.
+class ImpressionObserver : public WebContentsObserver {
+ public:
+ explicit ImpressionObserver(WebContents* contents)
+ : WebContentsObserver(contents) {}
+
+ // WebContentsObserver
+ void DidFinishNavigation(NavigationHandle* navigation_handle) override {
+ if (!navigation_handle->GetImpression()) {
+ if (waiting_for_null_impression_)
+ impression_loop_.Quit();
+ return;
+ }
+
+ last_impression_ = *(navigation_handle->GetImpression());
+
+ if (!waiting_for_null_impression_)
+ impression_loop_.Quit();
+ }
+
+ const Impression& last_impression() { return *last_impression_; }
+
+ const Impression& WaitForImpression() {
+ impression_loop_.Run();
+ return last_impression();
+ }
+
+ bool WaitForNavigationWithNoImpression() {
+ waiting_for_null_impression_ = true;
+ impression_loop_.Run();
+ waiting_for_null_impression_ = false;
+ return true;
+ }
+
+ private:
+ base::Optional<Impression> last_impression_;
+ bool waiting_for_null_impression_ = false;
+ base::RunLoop impression_loop_;
+};
+
+class ImpressionDeclarationBrowserTest : public ContentBrowserTest {
+ public:
+ ImpressionDeclarationBrowserTest() {
+ feature_list_.InitAndEnableFeature(features::kConversionMeasurement);
+ }
+
+ void SetUpOnMainThread() override {
+ host_resolver()->AddRule("*", "127.0.0.1");
+ embedded_test_server()->ServeFilesFromSourceDirectory(
+ "content/test/data/conversions");
+ embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
+ content::SetupCrossSiteRedirector(embedded_test_server());
+ ASSERT_TRUE(embedded_test_server()->Start());
+
+ https_server_ = std::make_unique<net::EmbeddedTestServer>(
+ net::EmbeddedTestServer::TYPE_HTTPS);
+ https_server_->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
+ net::test_server::RegisterDefaultHandlers(https_server_.get());
+ https_server_->ServeFilesFromSourceDirectory(
+ "content/test/data/conversions");
+ https_server_->ServeFilesFromSourceDirectory("content/test/data");
+ SetupCrossSiteRedirector(https_server_.get());
+ ASSERT_TRUE(https_server_->Start());
+ }
+
+ WebContents* web_contents() { return shell()->web_contents(); }
+
+ net::EmbeddedTestServer* https_server() { return https_server_.get(); }
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+ std::unique_ptr<net::EmbeddedTestServer> https_server_;
+};
+
+IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest,
+ ImpressionTagClicked_ImpressionReceived) {
+ ImpressionObserver impression_observer(web_contents());
+ GURL page_url =
+ https_server()->GetURL("b.test", "/page_with_impression_creator.html");
+ EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
+
+ // Create an anchor tag with impression attributes and click the link. By
+ // default the target is set to "_top".
+ EXPECT_TRUE(ExecJs(web_contents(), R"(
+ createImpressionTagWithReportingAndExpiry("link" /* id */,
+ "page_with_conversion_redirect.html" /* url */,
+ "1" /* impression data */,
+ "https://a.com" /* conversion_destination */,
+ "https://report.com" /* report_origin */,
+ 1000 /* expiry */);)"));
+ EXPECT_TRUE(ExecJs(shell(), "simulateClick(\'link\');"));
+
+ // Wait for the impression to be seen by the observer.
+ Impression last_impression = impression_observer.WaitForImpression();
+
+ // Verify the attributes of the impression are set as expected.
+ EXPECT_EQ(1UL, last_impression.impression_data);
+ EXPECT_EQ(url::Origin::Create(GURL("https://a.com")),
+ last_impression.conversion_destination);
+ EXPECT_EQ(url::Origin::Create(GURL("https://report.com")),
+ last_impression.reporting_origin);
+ EXPECT_EQ(base::TimeDelta::FromMilliseconds(1000), *last_impression.expiry);
+}
+
+IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest,
+ ImpressionTagNavigatesRemoteFrame_ImpressionReceived) {
+ EXPECT_TRUE(NavigateToURL(
+ web_contents(),
+ https_server()->GetURL("b.test", "/page_with_impression_creator.html")));
+
+ ShellAddedObserver new_shell_observer;
+
+ // Create an impression tag with a target frame that does not exist, which
+ // will open a new window to navigate.
+ EXPECT_TRUE(ExecJs(web_contents(), R"(
+ createImpressionTagWithTarget("link",
+ "page_with_conversion_redirect.html",
+ "1" /* impression data */,
+ "https://a.com" /* conversion_destination */,
+ "target" /* target */);)"));
+ EXPECT_TRUE(ExecJs(shell(), "simulateClick(\'link\');"));
+
+ ImpressionObserver impression_observer(
+ new_shell_observer.GetShell()->web_contents());
+
+ // Wait for the impression to be seen by the observer.
+ Impression last_impression = impression_observer.WaitForImpression();
+ EXPECT_EQ(1UL, impression_observer.last_impression().impression_data);
+}
+
+// Test frequently flakes due to timeout. ( https://crbug.com/1084201 )
+IN_PROC_BROWSER_TEST_F(
+ ImpressionDeclarationBrowserTest,
+ DISABLED_ImpressionTagNavigatesExistingRemoteFrame_ImpressionReceived) {
+ EXPECT_TRUE(NavigateToURL(
+ web_contents(),
+ https_server()->GetURL("b.test", "/page_with_impression_creator.html")));
+
+ WebContents* initial_web_contents = web_contents();
+
+ ShellAddedObserver new_shell_observer;
+ GURL remote_url = https_server()->GetURL("c.test", "/title1.html");
+ EXPECT_TRUE(ExecJs(web_contents(),
+ JsReplace("window.open($1, 'target');", remote_url)));
+
+ // Get the new web contents associated with the remote frame.
+ WebContents* remote_web_contents =
+ new_shell_observer.GetShell()->web_contents();
+
+ // Click on the impression and target the existing remote frame.
+ EXPECT_TRUE(ExecJs(initial_web_contents, R"(
+ createImpressionTagWithTarget("link",
+ "page_with_conversion_redirect.html",
+ "1" /* impression data */,
+ "https://a.com" /* conversion_destination */,
+ "target" /* target */);)"));
+ EXPECT_TRUE(ExecJs(shell(), "simulateClick(\'link\');"));
+
+ ImpressionObserver impression_observer(remote_web_contents);
+
+ // Wait for the impression to be seen by the observer.
+ Impression last_impression = impression_observer.WaitForImpression();
+ EXPECT_EQ(1UL, impression_observer.last_impression().impression_data);
+}
+
+IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest,
+ ImpressionTagWithOutOfBoundData_DefaultedTo0) {
+ ImpressionObserver impression_observer(web_contents());
+ EXPECT_TRUE(NavigateToURL(
+ web_contents(),
+ https_server()->GetURL("b.test", "/page_with_impression_creator.html")));
+
+ // The provided data overflows an unsigned 64 bit int, and should be handled
+ // properly.
+ EXPECT_TRUE(ExecJs(web_contents(), R"(
+ createImpressionTag("link",
+ "page_with_conversion_redirect.html",
+ "FFFFFFFFFFFFFFFFFFFFFF" /* impression data */,
+ "https://a.com" /* conversion_destination */);)"));
+ EXPECT_TRUE(ExecJs(shell(), "simulateClick(\'link\');"));
+
+ // Wait for the impression to be seen by the observer.
+ Impression last_impression = impression_observer.WaitForImpression();
+ EXPECT_EQ(0UL, impression_observer.last_impression().impression_data);
+}
+
+IN_PROC_BROWSER_TEST_F(
+ ImpressionDeclarationBrowserTest,
+ ImpressionTagNavigatesFromMiddleClick_ImpressionReceived) {
+ GURL page_url =
+ https_server()->GetURL("b.test", "/page_with_impression_creator.html");
+ EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
+
+ ShellAddedObserver new_shell_observer;
+
+ // Create an impression tag that is opened via middle click. This navigates in
+ // a new WebContents.
+ EXPECT_TRUE(ExecJs(web_contents(), R"(
+ createImpressionTag("link",
+ "page_with_conversion_redirect.html",
+ "1" /* impression data */,
+ "https://a.com" /* conversion_destination */);)"));
+ EXPECT_TRUE(ExecJs(shell(), "simulateMiddleClick(\'link\');"));
+
+ ImpressionObserver impression_observer(
+ new_shell_observer.GetShell()->web_contents());
+
+ Impression last_impression = impression_observer.WaitForImpression();
+
+ // Verify the attributes of the impression are set as expected.
+ EXPECT_EQ(1UL, last_impression.impression_data);
+}
+
+IN_PROC_BROWSER_TEST_F(
+ ImpressionDeclarationBrowserTest,
+ ImpressionTagNavigatesFromEnterPress_ImpressionReceived) {
+ GURL page_url =
+ https_server()->GetURL("b.test", "/page_with_impression_creator.html");
+ EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
+
+ EXPECT_TRUE(ExecJs(web_contents(), R"(
+ createImpressionTag("link",
+ "page_with_conversion_redirect.html",
+ "1" /* impression data */,
+ "https://a.com" /* conversion_destination */);)"));
+
+ // Focus the element, wait for it to receive focus, and simulate an enter
+ // press.
+ base::string16 expected_title = base::ASCIIToUTF16("focused");
+ content::TitleWatcher title_watcher(web_contents(), expected_title);
+ EXPECT_TRUE(ExecJs(shell(), R"(
+ let link = document.getElementById('link');
+ link.addEventListener('focus', function() { document.title = 'focused'; });
+ link.focus();)"));
+ EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle());
+ content::SimulateKeyPress(web_contents(), ui::DomKey::ENTER,
+ ui::DomCode::ENTER, ui::VKEY_RETURN, false, false,
+ false, false);
+
+ ImpressionObserver impression_observer(web_contents());
+ Impression last_impression = impression_observer.WaitForImpression();
+
+ // Verify the attributes of the impression are set as expected.
+ EXPECT_EQ(1UL, last_impression.impression_data);
+}
+
+IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest,
+ ImpressionOnInsecureSite_NotRegistered) {
+ // Navigate to a page with the non-https server.
+ EXPECT_TRUE(NavigateToURL(
+ web_contents(), embedded_test_server()->GetURL(
+ "b.test", "/page_with_impression_creator.html")));
+
+ ImpressionObserver impression_observer(web_contents());
+ EXPECT_TRUE(ExecJs(web_contents(), R"(
+ createImpressionTag("link",
+ "page_with_conversion_redirect.html",
+ "1" /* impression data */,
+ "https://a.com" /* conversion_destination */);)"));
+ EXPECT_TRUE(ExecJs(shell(), "simulateClick(\'link\');"));
+
+ // We should see a null impression on the navigation
+ EXPECT_TRUE(impression_observer.WaitForNavigationWithNoImpression());
+}
+
+IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest,
+ ImpressionWithInsecureDestination_NotRegistered) {
+ // Navigate to a page with the non-https server.
+ EXPECT_TRUE(NavigateToURL(
+ web_contents(),
+ https_server()->GetURL("b.test", "/page_with_impression_creator.html")));
+
+ ImpressionObserver impression_observer(web_contents());
+ EXPECT_TRUE(ExecJs(web_contents(), R"(
+ createImpressionTag("link",
+ "page_with_conversion_redirect.html",
+ "1" /* impression data */,
+ "http://a.com" /* conversion_destination */);)"));
+ EXPECT_TRUE(ExecJs(shell(), "simulateClick(\'link\');"));
+
+ // We should see a null impression on the navigation
+ EXPECT_TRUE(impression_observer.WaitForNavigationWithNoImpression());
+}
+
+IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest,
+ ImpressionWithInsecureReportingOrigin_NotRegistered) {
+ // Navigate to a page with the non-https server.
+ EXPECT_TRUE(NavigateToURL(
+ web_contents(),
+ https_server()->GetURL("b.test", "/page_with_impression_creator.html")));
+
+ ImpressionObserver impression_observer(web_contents());
+ EXPECT_TRUE(ExecJs(web_contents(), R"(
+ createImpressionTagWithReportingAndExpiry("link",
+ "page_with_conversion_redirect.html",
+ "1" /* impression data */,
+ "https://a.com" /* conversion_destination */,
+ "http://reporting.com" /* report_origin */,
+ 1000 /* expiry */);)"));
+ EXPECT_TRUE(ExecJs(shell(), "simulateClick(\'link\');"));
+
+ // We should see a null impression on the navigation
+ EXPECT_TRUE(impression_observer.WaitForNavigationWithNoImpression());
+}
+
+IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest,
+ ImpressionWithFeaturePolicyDisabled_NotRegistered) {
+ EXPECT_TRUE(NavigateToURL(
+ web_contents(),
+ https_server()->GetURL(
+ "b.test", "/page_with_conversion_measurement_disabled.html")));
+
+ ImpressionObserver impression_observer(web_contents());
+ EXPECT_TRUE(ExecJs(web_contents(), R"(
+ createImpressionTag("link",
+ "page_with_conversion_redirect.html",
+ "1" /* impression data */,
+ "https://a.com" /* conversion_destination */);)"));
+ EXPECT_TRUE(ExecJs(shell(), "simulateClick('link');"));
+
+ // We should see a null impression on the navigation
+ EXPECT_TRUE(impression_observer.WaitForNavigationWithNoImpression());
+}
+
+IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest,
+ ImpressionInSubframeWithoutFeaturePolicy_NotRegistered) {
+ GURL page_url = https_server()->GetURL("b.test", "/page_with_iframe.html");
+ EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
+
+ GURL subframe_url =
+ https_server()->GetURL("c.test", "/page_with_impression_creator.html");
+ NavigateIframeToURL(web_contents(), "test_iframe", subframe_url);
+
+ ImpressionObserver impression_observer(web_contents());
+ RenderFrameHost* subframe = ChildFrameAt(web_contents()->GetMainFrame(), 0);
+ EXPECT_TRUE(ExecJs(subframe, R"(
+ createImpressionTag("link",
+ "page_with_conversion_redirect.html",
+ "1" /* impression data */,
+ "https://a.com" /* conversion_destination */);)"));
+ EXPECT_TRUE(ExecJs(subframe, "simulateClick('link');"));
+
+ // We should see a null impression on the navigation
+ EXPECT_TRUE(impression_observer.WaitForNavigationWithNoImpression());
+}
+
+IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest,
+ ImpressionInSubframeWithFeaturePolicy_Registered) {
+ GURL page_url = https_server()->GetURL("b.test", "/page_with_iframe.html");
+ EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
+ EXPECT_TRUE(ExecJs(shell(), R"(
+ let frame = document.getElementById('test_iframe');
+ frame.setAttribute('allow', 'conversion-measurement');)"));
+
+ GURL subframe_url =
+ https_server()->GetURL("c.test", "/page_with_impression_creator.html");
+ NavigateIframeToURL(web_contents(), "test_iframe", subframe_url);
+
+ ImpressionObserver impression_observer(web_contents());
+ RenderFrameHost* subframe = ChildFrameAt(web_contents()->GetMainFrame(), 0);
+ EXPECT_TRUE(ExecJs(subframe, R"(
+ createImpressionTag("link",
+ "page_with_conversion_redirect.html",
+ "1" /* impression data */,
+ "https://a.com" /* conversion_destination */);)"));
+ EXPECT_TRUE(ExecJs(subframe, "simulateClick('link');"));
+
+ // We should see a null impression on the navigation
+ EXPECT_EQ(1u, impression_observer.WaitForImpression().impression_data);
+}
+
+IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest,
+ ImpressionNavigationReloads_NoImpression) {
+ EXPECT_TRUE(NavigateToURL(
+ web_contents(),
+ https_server()->GetURL("b.test", "/page_with_impression_creator.html")));
+
+ ImpressionObserver impression_observer(web_contents());
+ EXPECT_TRUE(ExecJs(web_contents(), R"(
+ createImpressionTag("link",
+ "page_with_conversion_redirect.html",
+ "1" /* impression data */,
+ "https://a.com" /* conversion_destination */);)"));
+ EXPECT_TRUE(ExecJs(shell(), "simulateClick(\'link\');"));
+ EXPECT_EQ(1UL, impression_observer.WaitForImpression().impression_data);
+
+ ImpressionObserver reload_observer(web_contents());
+ shell()->Reload();
+
+ // The reload navigation should not have an impression set.
+ EXPECT_TRUE(reload_observer.WaitForNavigationWithNoImpression());
+}
+
+// Same as the above test but via a renderer initiated reload.
+IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest,
+ RendererReloadImpressionNavigation_NoImpression) {
+ EXPECT_TRUE(NavigateToURL(
+ web_contents(),
+ https_server()->GetURL("b.test", "/page_with_impression_creator.html")));
+
+ ImpressionObserver impression_observer(web_contents());
+ EXPECT_TRUE(ExecJs(web_contents(), R"(
+ createImpressionTag("link",
+ "page_with_conversion_redirect.html",
+ "1" /* impression data */,
+ "https://a.com" /* conversion_destination */);)"));
+ EXPECT_TRUE(ExecJs(shell(), "simulateClick(\'link\');"));
+ EXPECT_EQ(1UL, impression_observer.WaitForImpression().impression_data);
+
+ ImpressionObserver reload_observer(web_contents());
+ EXPECT_TRUE(ExecJs(web_contents(), "window.location.reload()"));
+
+ // The reload navigation should not have an impression set.
+ EXPECT_TRUE(reload_observer.WaitForNavigationWithNoImpression());
+}
+
+IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest,
+ BackNavigateToImpressionNavigation_NoImpression) {
+ EXPECT_TRUE(NavigateToURL(
+ web_contents(),
+ https_server()->GetURL("b.test", "/page_with_impression_creator.html")));
+
+ ImpressionObserver impression_observer(web_contents());
+
+ // Click the default impression on the page.
+ EXPECT_TRUE(ExecJs(shell(), "simulateClick(\'impression_tag\');"));
+ EXPECT_EQ(1UL, impression_observer.WaitForImpression().impression_data);
+
+ // Navigate away so we can back navigate to the impression's navigated page.
+ EXPECT_TRUE(NavigateToURL(web_contents(), GURL("about:blank")));
+
+ // The back navigation should not have an impression set.
+ ImpressionObserver back_nav_observer(web_contents());
+ shell()->GoBackOrForward(-1);
+ EXPECT_TRUE(back_nav_observer.WaitForNavigationWithNoImpression());
+
+ // Navigate back to the original page and ensure subsequent clicks also log
+ // impressions.
+ ImpressionObserver second_back_nav_observer(web_contents());
+ shell()->GoBackOrForward(-1);
+ EXPECT_TRUE(second_back_nav_observer.WaitForNavigationWithNoImpression());
+
+ // Wait for the page to load and render the impression tag.
+ WaitForLoadStop(web_contents());
+ ImpressionObserver second_impression_observer(web_contents());
+ EXPECT_TRUE(ExecJs(shell(), "simulateClick(\'impression_tag\');"));
+ EXPECT_EQ(1UL,
+ second_impression_observer.WaitForImpression().impression_data);
+}
+
+} // namespace content
diff --git a/chromium/content/browser/conversions/storable_conversion.cc b/chromium/content/browser/conversions/storable_conversion.cc
index d3db20c1a7b..215554be145 100644
--- a/chromium/content/browser/conversions/storable_conversion.cc
+++ b/chromium/content/browser/conversions/storable_conversion.cc
@@ -4,7 +4,7 @@
#include "content/browser/conversions/storable_conversion.h"
-#include "base/logging.h"
+#include "base/check.h"
namespace content {
diff --git a/chromium/content/browser/conversions/storable_impression.cc b/chromium/content/browser/conversions/storable_impression.cc
index 0236f690782..ae300e47a7e 100644
--- a/chromium/content/browser/conversions/storable_impression.cc
+++ b/chromium/content/browser/conversions/storable_impression.cc
@@ -4,7 +4,7 @@
#include "content/browser/conversions/storable_impression.h"
-#include "base/logging.h"
+#include "base/check_op.h"
namespace content {
diff --git a/chromium/content/browser/conversions/storable_impression.h b/chromium/content/browser/conversions/storable_impression.h
index ccbc0d9ffa7..e6cea3ffbf5 100644
--- a/chromium/content/browser/conversions/storable_impression.h
+++ b/chromium/content/browser/conversions/storable_impression.h
@@ -56,6 +56,9 @@ class CONTENT_EXPORT StorableImpression {
// If null, an ID has not been assigned yet.
base::Optional<int64_t> impression_id_;
+
+ // When adding new members, the ImpressionsEqual() testing utility in
+ // conversion_test_utils.h should also be updated.
};
} // namespace content