blob: 1e5a70e81ec04a42520e42aecbaa9c7ecbb7ac62 [file] [log] [blame]
// Copyright 2019 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/metrics/structured/structured_metrics_provider.h"
#include "base/files/file_path.h"
#include "base/files/important_file_writer.h"
#include "base/files/scoped_temp_dir.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "base/values.h"
#include "components/metrics/structured/event_base.h"
#include "components/metrics/structured/recorder.h"
#include "components/metrics/structured/structured_events.h"
#include "components/prefs/json_pref_store.h"
#include "components/prefs/persistent_pref_store.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/metrics_proto/chrome_user_metrics_extension.pb.h"
namespace metrics {
namespace structured {
namespace {
// These event and metric names are used for testing.
// - event: TestEventOne
// - metric: TestMetricOne
// - metric: TestMetricTwo
// - event: TestsEventTwo
// - metric: TestMetricThree
// To test that the right values are calculated for hashed metrics, we need to
// set up some fake keys that we know the output hashes for. kKeyData contains
// the JSON for a simple structured_metrics.json file with keys for the test
// events. The two keys are ID'd by the name hashes of "TestEventOne" and
// "TestProject", because TestEventTwo is associated with TestProject.
// TODO(crbug.com/1016655): Once custom rotation periods have been implemented,
// change the large constants to 0.
constexpr char kKeyData[] = R"({
"keys":{
"15619026293081468407":{
"rotation_period":1000000,
"last_rotation":1000000,
"key":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"17426425568333718899":{
"rotation_period":1000000,
"last_rotation":1000000,
"key":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
}
}
})";
// The name hash of "TestEventOne".
constexpr uint64_t kEventOneHash = UINT64_C(15619026293081468407);
// The name hash of "TestEventTwo".
constexpr uint64_t kEventTwoHash = UINT64_C(15791833939776536363);
// The name hash of "TestEventThree".
constexpr uint64_t kEventThreeHash = UINT64_C(16464330721839207086);
// The name hash of "TestMetricOne".
constexpr uint64_t kMetricOneHash = UINT64_C(637929385654885975);
// The name hash of "TestMetricTwo".
constexpr uint64_t kMetricTwoHash = UINT64_C(14083999144141567134);
// The name hash of "TestMetricThree".
constexpr uint64_t kMetricThreeHash = UINT64_C(13469300759843809564);
// The hex-encoded first 8 bytes of SHA256("aaa...a")
constexpr char kKeyOneId[] = "3BA3F5F43B926026";
// The hex-encoded first 8 bytes of SHA256("bbb...b")
constexpr char kKeyTwoId[] = "BDB339768BC5E4FE";
// Test values.
constexpr char kValueOne[] = "value one";
constexpr char kValueTwo[] = "value two";
std::string HashToHex(const uint64_t hash) {
return base::HexEncode(&hash, sizeof(uint64_t));
}
} // namespace
class StructuredMetricsProviderTest : public testing::Test {
protected:
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
storage_ = new JsonPrefStore(temp_dir_.GetPath().Append("storage.json"));
Recorder::GetInstance()->SetUiTaskRunner(
task_environment_.GetMainThreadTaskRunner());
}
base::FilePath TempDirPath() { return temp_dir_.GetPath(); }
void Wait() { task_environment_.RunUntilIdle(); }
void WriteTestingKeys() {
CHECK(base::ImportantFileWriter::WriteFileAtomically(
TempDirPath().Append("structured_metrics.json"), kKeyData,
"StructuredMetricsProviderTest"));
}
// Simulates the three external events that the structure metrics system cares
// about: the metrics service initializing and enabling its providers, and a
// user logging in.
void Init() {
// Create the provider, normally done by the ChromeMetricsServiceClient.
provider_ = std::make_unique<StructuredMetricsProvider>();
// Enable recording, normally done after the metrics service has checked
// consent allows recording.
provider_->OnRecordingEnabled();
// Add a profile, normally done by the ChromeMetricsServiceClient after a
// user logs in.
provider_->OnProfileAdded(TempDirPath());
Wait();
}
bool is_initialized() { return provider_->initialized_; }
bool is_recording_enabled() { return provider_->recording_enabled_; }
void OnRecordingEnabled() { provider_->OnRecordingEnabled(); }
void OnRecordingDisabled() { provider_->OnRecordingDisabled(); }
void OnProfileAdded(const base::FilePath& path) {
provider_->OnProfileAdded(path);
}
void CommitPendingWrite() {
provider_->CommitPendingWriteForTest();
Wait();
}
ChromeUserMetricsExtension GetProvidedEvents() {
ChromeUserMetricsExtension uma_proto;
provider_->ProvideCurrentSessionData(&uma_proto);
return uma_proto;
}
// Most tests start without an existing structured_metrics.json storage file
// on-disk, and so will trigger a single PREF_READ_ERROR_NO_FILE metric.
// Expect that, and no other errors.
void ExpectOnlyFileReadError() {
histogram_tester_.ExpectTotalCount("UMA.StructuredMetrics.InternalError",
0);
histogram_tester_.ExpectUniqueSample(
"UMA.StructuredMetrics.PrefReadError",
PersistentPrefStore::PREF_READ_ERROR_NO_FILE, 1);
}
protected:
std::unique_ptr<StructuredMetricsProvider> provider_;
base::HistogramTester histogram_tester_;
private:
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::MainThreadType::UI,
base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED};
base::ScopedTempDir temp_dir_;
scoped_refptr<JsonPrefStore> storage_;
};
// Simple test to ensure initialization works correctly in the case of a
// first-time run.
TEST_F(StructuredMetricsProviderTest, ProviderInitializesFromBlankSlate) {
Init();
EXPECT_TRUE(is_initialized());
EXPECT_TRUE(is_recording_enabled());
ExpectOnlyFileReadError();
}
// Ensure a call to OnRecordingDisabled prevents reporting.
TEST_F(StructuredMetricsProviderTest, EventsNotReportedWhenRecordingDisabled) {
Init();
OnRecordingDisabled();
events::TestEventOne().SetTestMetricTwo(1).Record();
EXPECT_EQ(GetProvidedEvents().structured_event_size(), 0);
ExpectOnlyFileReadError();
}
// Ensure that, if recording is disabled part-way through initialization, the
// initialization still completes correctly, but recording is correctly set to
// disabled.
TEST_F(StructuredMetricsProviderTest, RecordingDisabledDuringInitialization) {
provider_ = std::make_unique<StructuredMetricsProvider>();
OnProfileAdded(TempDirPath());
OnRecordingDisabled();
EXPECT_FALSE(is_initialized());
EXPECT_FALSE(is_recording_enabled());
Wait();
EXPECT_TRUE(is_initialized());
EXPECT_FALSE(is_recording_enabled());
ExpectOnlyFileReadError();
}
// Ensure that recording is disabled until explicitly enabled with a call to
// OnRecordingEnabled.
TEST_F(StructuredMetricsProviderTest, RecordingDisabledByDefault) {
provider_ = std::make_unique<StructuredMetricsProvider>();
OnProfileAdded(TempDirPath());
Wait();
EXPECT_TRUE(is_initialized());
EXPECT_FALSE(is_recording_enabled());
OnRecordingEnabled();
EXPECT_TRUE(is_recording_enabled());
ExpectOnlyFileReadError();
}
TEST_F(StructuredMetricsProviderTest, RecordedEventAppearsInReport) {
Init();
events::TestEventOne()
.SetTestMetricOne("a string")
.SetTestMetricTwo(12345)
.Record();
events::TestEventOne()
.SetTestMetricOne("a string")
.SetTestMetricTwo(12345)
.Record();
events::TestEventOne()
.SetTestMetricOne("a string")
.SetTestMetricTwo(12345)
.Record();
EXPECT_EQ(GetProvidedEvents().structured_event_size(), 3);
ExpectOnlyFileReadError();
}
TEST_F(StructuredMetricsProviderTest, EventsReportedCorrectly) {
WriteTestingKeys();
Init();
events::TestEventOne()
.SetTestMetricOne(kValueOne)
.SetTestMetricTwo(12345)
.Record();
events::TestEventTwo().SetTestMetricThree(kValueTwo).Record();
const auto uma = GetProvidedEvents();
ASSERT_EQ(uma.structured_event_size(), 2);
{ // First event
const auto& event = uma.structured_event(0);
EXPECT_EQ(event.event_name_hash(), kEventOneHash);
EXPECT_EQ(HashToHex(event.profile_event_id()), kKeyOneId);
ASSERT_EQ(event.metrics_size(), 2);
{ // First metric
const auto& metric = event.metrics(0);
EXPECT_EQ(metric.name_hash(), kMetricOneHash);
EXPECT_EQ(HashToHex(metric.value_hmac()),
// Value of HMAC_256("aaa...a", concat(hex(kMetricOneHash),
// "value one"))
"8C2469269D142715");
}
{ // Second metric
const auto& metric = event.metrics(1);
EXPECT_EQ(metric.name_hash(), kMetricTwoHash);
EXPECT_EQ(metric.value_int64(), 12345);
}
}
{ // Second event
const auto& event = uma.structured_event(1);
EXPECT_EQ(event.event_name_hash(), kEventTwoHash);
EXPECT_EQ(HashToHex(event.profile_event_id()), kKeyTwoId);
ASSERT_EQ(event.metrics_size(), 1);
{ // First metric
const auto& metric = event.metrics(0);
EXPECT_EQ(metric.name_hash(), kMetricThreeHash);
EXPECT_EQ(HashToHex(metric.value_hmac()),
// Value of HMAC_256("bbb...b", concat(hex(kProjectHash),
// "value three"))
"86F0169868588DC7");
}
}
histogram_tester_.ExpectTotalCount("UMA.StructuredMetrics.InternalError", 0);
histogram_tester_.ExpectTotalCount("UMA.StructuredMetrics.PrefReadError", 0);
}
TEST_F(StructuredMetricsProviderTest, EventsWithinProjectReportedWithSameID) {
WriteTestingKeys();
Init();
events::TestEventOne().Record();
events::TestEventTwo().Record();
events::TestEventThree().Record();
const auto uma = GetProvidedEvents();
ASSERT_EQ(uma.structured_event_size(), 3);
const auto& event_one = uma.structured_event(0);
const auto& event_two = uma.structured_event(1);
const auto& event_three = uma.structured_event(2);
// Check events are in the right order.
EXPECT_EQ(event_one.event_name_hash(), kEventOneHash);
EXPECT_EQ(event_two.event_name_hash(), kEventTwoHash);
EXPECT_EQ(event_three.event_name_hash(), kEventThreeHash);
// Events two and three share a project, so should have the same ID. Event
// one should have its own ID.
EXPECT_EQ(HashToHex(event_one.profile_event_id()), kKeyOneId);
EXPECT_EQ(HashToHex(event_two.profile_event_id()), kKeyTwoId);
EXPECT_EQ(HashToHex(event_three.profile_event_id()), kKeyTwoId);
histogram_tester_.ExpectTotalCount("UMA.StructuredMetrics.InternalError", 0);
histogram_tester_.ExpectTotalCount("UMA.StructuredMetrics.PrefReadError", 0);
}
// Test that a call to ProvideCurrentSessionData clears the provided events from
// the cache, and a subsequent call does not return those events again.
TEST_F(StructuredMetricsProviderTest, EventsClearedAfterReport) {
Init();
events::TestEventOne().SetTestMetricTwo(1).Record();
events::TestEventOne().SetTestMetricTwo(2).Record();
// Should provide both the previous events.
EXPECT_EQ(GetProvidedEvents().structured_event_size(), 2);
// But the previous events shouldn't appear in the second report.
EXPECT_EQ(GetProvidedEvents().structured_event_size(), 0);
events::TestEventOne().SetTestMetricTwo(3).Record();
// The third request should only contain the third event.
EXPECT_EQ(GetProvidedEvents().structured_event_size(), 1);
ExpectOnlyFileReadError();
}
// Test that events recorded in one session are correctly persisted and are
// uploaded in the first report from a subsequent session.
TEST_F(StructuredMetricsProviderTest, EventsFromPreviousSessionAreReported) {
// Start first session and record one event.
Init();
events::TestEventOne().SetTestMetricTwo(1234).Record();
// Write events to disk, then destroy the provider.
CommitPendingWrite();
provider_.reset();
// Start a second session and ensure the event is reported.
Init();
const auto uma = GetProvidedEvents();
ASSERT_EQ(uma.structured_event_size(), 1);
ASSERT_EQ(uma.structured_event(0).metrics_size(), 1);
EXPECT_EQ(uma.structured_event(0).metrics(0).value_int64(), 1234);
ExpectOnlyFileReadError();
}
// Test that events reported at various stages before and during initialization
// are ignored (and don't cause a crash).
TEST_F(StructuredMetricsProviderTest, EventsNotRecordedBeforeInitialization) {
// Manually create and initialize the provider, adding recording calls between
// each step. All of these events should be ignored.
events::TestEventOne().SetTestMetricTwo(1).Record();
provider_ = std::make_unique<StructuredMetricsProvider>();
events::TestEventOne().SetTestMetricTwo(1).Record();
OnRecordingEnabled();
events::TestEventOne().SetTestMetricTwo(1).Record();
OnProfileAdded(TempDirPath());
// This one should still fail even though all of the initialization calls are
// done, because the provider hasn't finished loading the keys from disk.
events::TestEventOne().SetTestMetricTwo(1).Record();
Wait();
EXPECT_EQ(GetProvidedEvents().structured_event_size(), 0);
ExpectOnlyFileReadError();
}
// Ensure a call to OnRecordingDisabled not only prevents the reporting of new
// events, but also clears the cache of any existing events that haven't yet
// been reported.
TEST_F(StructuredMetricsProviderTest,
ExistingEventsClearedWhenRecordingDisabled) {
Init();
events::TestEventOne().SetTestMetricTwo(1).Record();
events::TestEventOne().SetTestMetricTwo(1).Record();
OnRecordingDisabled();
events::TestEventOne().SetTestMetricTwo(1).Record();
EXPECT_EQ(GetProvidedEvents().structured_event_size(), 0);
ExpectOnlyFileReadError();
}
// Ensure that recording and reporting is re-enabled after recording is disabled
// and then enabled again.
TEST_F(StructuredMetricsProviderTest, ReportingResumesWhenEnabled) {
Init();
events::TestEventOne().SetTestMetricTwo(1).Record();
events::TestEventOne().SetTestMetricTwo(1).Record();
OnRecordingDisabled();
events::TestEventOne().SetTestMetricTwo(1).Record();
OnRecordingEnabled();
events::TestEventOne().SetTestMetricTwo(1).Record();
events::TestEventOne().SetTestMetricTwo(1).Record();
EXPECT_EQ(GetProvidedEvents().structured_event_size(), 2);
ExpectOnlyFileReadError();
}
// Ensure that a call to ProvideCurrentSessionData before initialization
// completes returns no events.
TEST_F(StructuredMetricsProviderTest,
ReportsNothingBeforeInitializationComplete) {
provider_ = std::make_unique<StructuredMetricsProvider>();
EXPECT_EQ(GetProvidedEvents().structured_event_size(), 0);
OnRecordingEnabled();
EXPECT_EQ(GetProvidedEvents().structured_event_size(), 0);
OnProfileAdded(TempDirPath());
EXPECT_EQ(GetProvidedEvents().structured_event_size(), 0);
}
} // namespace structured
} // namespace metrics