| // 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 |