blob: 4d1b7c34b06936e2ae73ac28609d86a18ee117ef [file] [log] [blame]
// Copyright 2018 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 "components/ukm/ukm_recorder_impl.h"
#include "base/bind.h"
#include "base/metrics/metrics_hashes.h"
#include "base/test/task_environment.h"
#include "components/ukm/scheme_constants.h"
#include "components/ukm/test_ukm_recorder.h"
#include "components/ukm/ukm_recorder_observer.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_entry_builder.h"
#include "services/metrics/public/cpp/ukm_source.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/metrics_proto/ukm/report.pb.h"
#include "url/gurl.h"
namespace ukm {
using TestEvent1 = builders::PageLoad;
const uint64_t kTestEntryHash = 1234;
const uint64_t kTestMetricsHash = 12345;
const char kTestEntryName[] = "TestEntry";
const char kTestMetrics[] = "TestMetrics";
std::map<uint64_t, builders::EntryDecoder> CreateTestingDecodeMap() {
return {
{kTestEntryHash,
{kTestEntryName,
{
{kTestMetricsHash, kTestMetrics},
}}},
};
}
// Helper class for testing UkmRecorderImpl observers.
class TestUkmObserver : public UkmRecorderObserver {
public:
explicit TestUkmObserver(UkmRecorderImpl* ukm_recorder_impl) {
base::flat_set<uint64_t> event_hashes = {kTestEntryHash};
ukm_recorder_impl->AddUkmRecorderObserver(event_hashes, this);
}
~TestUkmObserver() override = default;
// UkmRecorderImpl::UkmRecorderObserver override.
void OnEntryAdded(mojom::UkmEntryPtr entry) override {
if (stop_waiting_)
std::move(stop_waiting_).Run();
ASSERT_EQ(entry->event_hash, ukm_entry_->event_hash);
ASSERT_EQ(entry->source_id, ukm_entry_->source_id);
ASSERT_EQ(entry->metrics[kTestMetricsHash],
ukm_entry_->metrics[kTestMetricsHash]);
}
void OnUpdateSourceURL(SourceId source_id,
const std::vector<GURL>& urls) override {
if (stop_waiting_)
std::move(stop_waiting_).Run();
ASSERT_EQ(source_id_, source_id);
ASSERT_EQ(urls_, urls);
}
void OnPurgeRecordingsWithUrlScheme(const std::string& url_scheme) override {
if (stop_waiting_)
std::move(stop_waiting_).Run();
}
void OnPurge() override {
if (stop_waiting_)
std::move(stop_waiting_).Run();
}
void OnUkmAllowedStateChanged(bool allow) override {
if (stop_waiting_)
std::move(stop_waiting_).Run();
EXPECT_EQ(expected_allow_, allow);
}
void WaitOnUkmAllowedStateChanged(bool expected_allow) {
expected_allow_ = expected_allow;
WaitCallback();
}
void WaitAddEntryCallback(uint64_t event_hash, mojom::UkmEntryPtr ukm_entry) {
ukm_entry_ = std::move(ukm_entry);
WaitCallback();
}
void WaitUpdateSourceURLCallback(SourceId source_id,
const std::vector<GURL>& urls) {
source_id_ = source_id;
urls_ = urls;
WaitCallback();
}
void WaitCallback() {
base::RunLoop run_loop;
stop_waiting_ = run_loop.QuitClosure();
run_loop.Run();
}
private:
base::OnceClosure stop_waiting_;
mojom::UkmEntryPtr ukm_entry_;
SourceId source_id_;
std::vector<GURL> urls_;
bool expected_allow_ = false;
};
TEST(UkmRecorderImplTest, IsSampledIn) {
UkmRecorderImpl impl;
for (int i = 0; i < 100; ++i) {
// These are constant regardless of the seed, source, and event.
EXPECT_FALSE(impl.IsSampledIn(-i, i, 0));
EXPECT_TRUE(impl.IsSampledIn(-i, i, 1));
}
// These depend on the source, event, and initial seed. There's no real
// predictability here but should see roughly 50% true and 50% false with
// no obvious correlation and the same for every run of the test.
impl.SetSamplingSeedForTesting(123);
EXPECT_FALSE(impl.IsSampledIn(1, 1, 2));
EXPECT_TRUE(impl.IsSampledIn(1, 2, 2));
EXPECT_FALSE(impl.IsSampledIn(2, 1, 2));
EXPECT_TRUE(impl.IsSampledIn(2, 2, 2));
EXPECT_TRUE(impl.IsSampledIn(3, 1, 2));
EXPECT_FALSE(impl.IsSampledIn(3, 2, 2));
EXPECT_FALSE(impl.IsSampledIn(4, 1, 2));
EXPECT_TRUE(impl.IsSampledIn(4, 2, 2));
impl.SetSamplingSeedForTesting(456);
EXPECT_TRUE(impl.IsSampledIn(1, 1, 2));
EXPECT_FALSE(impl.IsSampledIn(1, 2, 2));
EXPECT_TRUE(impl.IsSampledIn(2, 1, 2));
EXPECT_FALSE(impl.IsSampledIn(2, 2, 2));
EXPECT_FALSE(impl.IsSampledIn(3, 1, 2));
EXPECT_TRUE(impl.IsSampledIn(3, 2, 2));
EXPECT_TRUE(impl.IsSampledIn(4, 1, 2));
EXPECT_FALSE(impl.IsSampledIn(4, 2, 2));
impl.SetSamplingSeedForTesting(789);
EXPECT_TRUE(impl.IsSampledIn(1, 1, 2));
EXPECT_FALSE(impl.IsSampledIn(1, 2, 2));
EXPECT_TRUE(impl.IsSampledIn(2, 1, 2));
EXPECT_FALSE(impl.IsSampledIn(2, 2, 2));
EXPECT_FALSE(impl.IsSampledIn(3, 1, 2));
EXPECT_TRUE(impl.IsSampledIn(3, 2, 2));
EXPECT_TRUE(impl.IsSampledIn(4, 1, 2));
EXPECT_FALSE(impl.IsSampledIn(4, 2, 2));
// Load a configuration for more detailed testing.
std::map<std::string, std::string> params = {
{"y.a", "3"},
{"y.b", "y.a"},
{"y.c", "y.a"},
};
impl.LoadExperimentSamplingParams(params);
EXPECT_LT(impl.default_sampling_rate_, 0);
// Functions under test take hashes instead of strings.
uint64_t hash_ya = base::HashMetricName("y.a");
uint64_t hash_yb = base::HashMetricName("y.b");
uint64_t hash_yc = base::HashMetricName("y.c");
// Check that the parameters are active.
EXPECT_TRUE(impl.IsSampledIn(11, hash_ya));
EXPECT_TRUE(impl.IsSampledIn(22, hash_ya));
EXPECT_FALSE(impl.IsSampledIn(33, hash_ya));
EXPECT_FALSE(impl.IsSampledIn(44, hash_ya));
EXPECT_FALSE(impl.IsSampledIn(55, hash_ya));
// Check that sampled in/out is the same for all three.
for (int source = 0; source < 100; ++source) {
bool sampled_in = impl.IsSampledIn(source, hash_ya);
EXPECT_EQ(sampled_in, impl.IsSampledIn(source, hash_yb));
EXPECT_EQ(sampled_in, impl.IsSampledIn(source, hash_yc));
}
}
TEST(UkmRecorderImplTest, PurgeExtensionRecordings) {
TestUkmRecorder recorder;
// Enable extension sync.
recorder.SetIsWebstoreExtensionCallback(
base::BindRepeating([](base::StringPiece) { return true; }));
// Record some sources and events.
SourceId id1 = ConvertToSourceId(1, SourceIdType::NAVIGATION_ID);
recorder.UpdateSourceURL(id1, GURL("https://www.google.ca"));
SourceId id2 = ConvertToSourceId(2, SourceIdType::NAVIGATION_ID);
recorder.UpdateSourceURL(id2, GURL("chrome-extension://abc/manifest.json"));
SourceId id3 = ConvertToSourceId(3, SourceIdType::NAVIGATION_ID);
recorder.UpdateSourceURL(id3, GURL("http://www.wikipedia.org"));
SourceId id4 = ConvertToSourceId(4, SourceIdType::NAVIGATION_ID);
recorder.UpdateSourceURL(id4, GURL("chrome-extension://abc/index.html"));
TestEvent1(id1).Record(&recorder);
TestEvent1(id2).Record(&recorder);
// All sources and events have been recorded.
EXPECT_TRUE(recorder.extensions_enabled_);
EXPECT_TRUE(recorder.recording_is_continuous_);
EXPECT_EQ(4U, recorder.sources().size());
EXPECT_EQ(2U, recorder.entries().size());
recorder.PurgeRecordingsWithUrlScheme(kExtensionScheme);
// Recorded sources of extension scheme and related events have been cleared.
EXPECT_EQ(2U, recorder.sources().size());
EXPECT_EQ(1U, recorder.sources().count(id1));
EXPECT_EQ(0U, recorder.sources().count(id2));
EXPECT_EQ(1U, recorder.sources().count(id3));
EXPECT_EQ(0U, recorder.sources().count(id4));
EXPECT_FALSE(recorder.recording_is_continuous_);
EXPECT_EQ(1U, recorder.entries().size());
EXPECT_EQ(id1, recorder.entries()[0]->source_id);
// Recording is disabled for extensions, thus new extension URL will not be
// recorded.
recorder.EnableRecording(/* extensions = */ false);
recorder.UpdateSourceURL(id4, GURL("chrome-extension://abc/index.html"));
EXPECT_FALSE(recorder.extensions_enabled_);
EXPECT_EQ(2U, recorder.sources().size());
}
TEST(UkmRecorderImplTest, WebApkSourceUrl) {
base::test::TaskEnvironment env;
ukm::TestAutoSetUkmRecorder test_ukm_recorder;
GURL url("https://example_url.com/manifest.json");
SourceId id = UkmRecorderImpl::GetSourceIdForWebApkManifestUrl(url);
ASSERT_NE(kInvalidSourceId, id);
const auto& sources = test_ukm_recorder.GetSources();
ASSERT_EQ(1ul, sources.size());
auto it = sources.find(id);
ASSERT_NE(sources.end(), it);
EXPECT_EQ(url, it->second->url());
EXPECT_EQ(1u, it->second->urls().size());
EXPECT_EQ(SourceIdType::WEBAPK_ID, GetSourceIdType(id));
}
TEST(UkmRecorderImplTest, PaymentAppScopeUrl) {
base::test::TaskEnvironment env;
ukm::TestAutoSetUkmRecorder test_ukm_recorder;
GURL url("https://bobpay.com");
SourceId id = UkmRecorderImpl::GetSourceIdForPaymentAppFromScope(url);
ASSERT_NE(kInvalidSourceId, id);
const auto& sources = test_ukm_recorder.GetSources();
ASSERT_EQ(1ul, sources.size());
auto it = sources.find(id);
ASSERT_NE(sources.end(), it);
EXPECT_EQ(url, it->second->url());
EXPECT_EQ(1u, it->second->urls().size());
EXPECT_EQ(SourceIdType::PAYMENT_APP_ID, GetSourceIdType(id));
}
// Tests that UkmRecorderObserver is notified on a new UKM entry.
TEST(UkmRecorderImplTest, ObserverNotifiedOnNewEntry) {
base::test::TaskEnvironment env;
ukm::TestAutoSetUkmRecorder test_ukm_recorder;
TestUkmObserver test_observer(&test_ukm_recorder);
test_ukm_recorder.decode_map_ = CreateTestingDecodeMap();
auto entry = mojom::UkmEntry::New();
entry->event_hash = kTestEntryHash;
entry->source_id = 345;
entry->metrics[kTestMetricsHash] = 10;
test_ukm_recorder.AddEntry(entry->Clone());
test_observer.WaitAddEntryCallback(kTestEntryHash, std::move(entry));
}
// Tests that UkmRecorderObserver is notified on source URL updates.
TEST(UkmRecorderImplTest, ObserverNotifiedOnSourceURLUpdate) {
base::test::TaskEnvironment env;
ukm::TestAutoSetUkmRecorder test_ukm_recorder;
TestUkmObserver test_observer(&test_ukm_recorder);
uint64_t source_id = 345;
GURL url("http://abc.com");
std::vector<GURL> urls;
urls.emplace_back(url);
test_ukm_recorder.UpdateSourceURL(source_id, url);
test_observer.WaitUpdateSourceURLCallback(source_id, urls);
}
// Tests that UkmRecorderObserver is notified on purge.
TEST(UkmRecorderImplTest, ObserverNotifiedOnPurge) {
base::test::TaskEnvironment env;
ukm::TestAutoSetUkmRecorder test_ukm_recorder;
TestUkmObserver test_observer(&test_ukm_recorder);
test_ukm_recorder.PurgeRecordingsWithUrlScheme(kExtensionScheme);
test_observer.WaitCallback();
test_ukm_recorder.Purge();
test_observer.WaitCallback();
}
TEST(UkmRecorderImplTest, ObserverNotifiedOnUkmAllowedStateChanged) {
base::test::TaskEnvironment env;
ukm::TestAutoSetUkmRecorder test_ukm_recorder;
TestUkmObserver test_observer(&test_ukm_recorder);
test_ukm_recorder.OnUkmAllowedStateChanged(false);
test_observer.WaitOnUkmAllowedStateChanged(false);
test_ukm_recorder.OnUkmAllowedStateChanged(true);
test_observer.WaitOnUkmAllowedStateChanged(true);
}
// Tests that adding and removing observers work as expected.
TEST(UkmRecorderImplTest, AddRemoveObserver) {
base::test::TaskEnvironment env;
ukm::TestAutoSetUkmRecorder test_ukm_recorder;
// Adding 3 observers, the first 2 oberserve the same event
// while the last one observes a different event.
UkmRecorderObserver obs1, obs2, obs3;
base::flat_set<uint64_t> events1 = {123};
test_ukm_recorder.AddUkmRecorderObserver(events1, &obs1);
test_ukm_recorder.AddUkmRecorderObserver(events1, &obs2);
base::flat_set<uint64_t> events2 = {345};
test_ukm_recorder.AddUkmRecorderObserver(events2, &obs3);
// Remove the first observer.
test_ukm_recorder.RemoveUkmRecorderObserver(&obs1);
{
base::AutoLock auto_lock(test_ukm_recorder.lock_);
ASSERT_FALSE(test_ukm_recorder.observers_.empty());
// There are still 2 separate events being observed, each
// has one observer now.
ASSERT_NE(test_ukm_recorder.observers_.find(events1),
test_ukm_recorder.observers_.end());
ASSERT_NE(test_ukm_recorder.observers_.find(events2),
test_ukm_recorder.observers_.end());
}
// Removing the 2nd observer.
test_ukm_recorder.RemoveUkmRecorderObserver(&obs2);
{
base::AutoLock auto_lock(test_ukm_recorder.lock_);
// Only the 2nd event is being observed now, the first
// event should be removed from the observers map.
ASSERT_EQ(test_ukm_recorder.observers_.find(events1),
test_ukm_recorder.observers_.end());
ASSERT_NE(test_ukm_recorder.observers_.find(events2),
test_ukm_recorder.observers_.end());
}
// Removing the last observer should clear the observer map.
test_ukm_recorder.RemoveUkmRecorderObserver(&obs3);
{
base::AutoLock auto_lock(test_ukm_recorder.lock_);
ASSERT_TRUE(test_ukm_recorder.observers_.empty());
}
}
} // namespace ukm