| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/metrics/extensions_metrics_provider.h" |
| |
| #include <stdint.h> |
| |
| #include <memory> |
| #include <optional> |
| #include <string> |
| |
| #include "base/containers/contains.h" |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/test/task_environment.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #include "chrome/browser/extensions/extension_service.h" |
| #include "chrome/browser/extensions/extension_service_test_base.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/test/base/testing_browser_process.h" |
| #include "chrome/test/base/testing_profile_manager.h" |
| #include "components/metrics/client_info.h" |
| #include "components/metrics/metrics_service.h" |
| #include "components/metrics/metrics_state_manager.h" |
| #include "components/metrics/test/test_enabled_state_provider.h" |
| #include "components/prefs/testing_pref_service.h" |
| #include "extensions/browser/blocklist_extension_prefs.h" |
| #include "extensions/browser/disable_reason.h" |
| #include "extensions/browser/extension_prefs.h" |
| #include "extensions/common/api/extension_action/action_info.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/extension_builder.h" |
| #include "extensions/common/extension_set.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/metrics_proto/extension_install.pb.h" |
| #include "third_party/metrics_proto/system_profile.pb.h" |
| |
| using extensions::Extension; |
| using extensions::ExtensionBuilder; |
| using extensions::Manifest; |
| using extensions::mojom::ManifestLocation; |
| using metrics::ExtensionInstallProto; |
| |
| namespace { |
| |
| class TestExtensionsMetricsProvider : public ExtensionsMetricsProvider { |
| public: |
| explicit TestExtensionsMetricsProvider( |
| metrics::MetricsStateManager* metrics_state_manager) |
| : ExtensionsMetricsProvider(metrics_state_manager) {} |
| |
| // Makes the protected HashExtension method available to testing code. |
| using ExtensionsMetricsProvider::HashExtension; |
| |
| protected: |
| // Override the GetInstalledExtensions method to return a set of extensions |
| // for tests. |
| std::optional<extensions::ExtensionSet> GetInstalledExtensions( |
| Profile* profile) override { |
| extensions::ExtensionSet extensions; |
| extensions.Insert(extensions::ExtensionBuilder() |
| .SetManifest(base::Value::Dict() |
| .Set("name", "Test extension") |
| .Set("version", "1.0.0") |
| .Set("manifest_version", 2)) |
| .SetID("ahfgeienlihckogmohjhadlkjgocpleb") |
| .Build()); |
| extensions.Insert(extensions::ExtensionBuilder() |
| .SetManifest(base::Value::Dict() |
| .Set("name", "Test extension 2") |
| .Set("version", "1.0.0") |
| .Set("manifest_version", 2)) |
| .SetID("pknkgggnfecklokoggaggchhaebkajji") |
| .Build()); |
| extensions.Insert(extensions::ExtensionBuilder() |
| .SetManifest(base::Value::Dict() |
| .Set("name", "Colliding Extension") |
| .Set("version", "1.0.0") |
| .Set("manifest_version", 2)) |
| .SetID("mdhofdjgenpkhlmddfaegdjddcecipmo") |
| .Build()); |
| return extensions; |
| } |
| |
| // Override GetClientID() to return a specific value on which test |
| // expectations are based. |
| uint64_t GetClientID() const override { return 0x3f1bfee9; } |
| }; |
| |
| } // namespace |
| |
| // Checks that the hash function used to hide precise extension IDs produces |
| // the expected values. |
| TEST(ExtensionsMetricsProvider, HashExtension) { |
| EXPECT_EQ(978, |
| TestExtensionsMetricsProvider::HashExtension( |
| "ahfgeienlihckogmohjhadlkjgocpleb", 0)); |
| EXPECT_EQ(10, |
| TestExtensionsMetricsProvider::HashExtension( |
| "ahfgeienlihckogmohjhadlkjgocpleb", 3817)); |
| EXPECT_EQ(1007, |
| TestExtensionsMetricsProvider::HashExtension( |
| "pknkgggnfecklokoggaggchhaebkajji", 3817)); |
| EXPECT_EQ(10, |
| TestExtensionsMetricsProvider::HashExtension( |
| "mdhofdjgenpkhlmddfaegdjddcecipmo", 3817)); |
| } |
| |
| // Checks that the fake set of extensions provided by |
| // TestExtensionsMetricsProvider is encoded properly. |
| TEST(ExtensionsMetricsProvider, SystemProtoEncoding) { |
| metrics::SystemProfileProto system_profile; |
| base::test::TaskEnvironment task_environment; |
| TestingProfileManager testing_profile_manager( |
| TestingBrowserProcess::GetGlobal()); |
| ASSERT_TRUE(testing_profile_manager.SetUp()); |
| TestingPrefServiceSimple local_state; |
| metrics::TestEnabledStateProvider enabled_state_provider(true, true); |
| metrics::MetricsService::RegisterPrefs(local_state.registry()); |
| std::unique_ptr<metrics::MetricsStateManager> metrics_state_manager( |
| metrics::MetricsStateManager::Create(&local_state, |
| &enabled_state_provider, |
| std::wstring(), base::FilePath())); |
| metrics_state_manager->InstantiateFieldTrialList(); |
| TestExtensionsMetricsProvider extension_metrics(metrics_state_manager.get()); |
| extension_metrics.ProvideSystemProfileMetrics(&system_profile); |
| ASSERT_EQ(2, system_profile.occupied_extension_bucket_size()); |
| EXPECT_EQ(10, system_profile.occupied_extension_bucket(0)); |
| EXPECT_EQ(1007, system_profile.occupied_extension_bucket(1)); |
| } |
| |
| class ExtensionMetricsProviderInstallsTest |
| : public extensions::ExtensionServiceTestBase { |
| public: |
| ExtensionMetricsProviderInstallsTest() {} |
| |
| ExtensionMetricsProviderInstallsTest( |
| const ExtensionMetricsProviderInstallsTest&) = delete; |
| ExtensionMetricsProviderInstallsTest& operator=( |
| const ExtensionMetricsProviderInstallsTest&) = delete; |
| |
| ~ExtensionMetricsProviderInstallsTest() override {} |
| |
| void SetUp() override { |
| ExtensionServiceTestBase::SetUp(); |
| InitializeEmptyExtensionService(); |
| prefs_ = extensions::ExtensionPrefs::Get(profile()); |
| |
| last_sample_time_ = base::Time::Now() - base::Minutes(30); |
| } |
| |
| ExtensionInstallProto ConstructProto(const Extension& extension) { |
| return ExtensionsMetricsProvider::ConstructInstallProtoForTesting( |
| extension, prefs_, last_sample_time_, profile()); |
| } |
| std::vector<ExtensionInstallProto> GetInstallsForProfile() { |
| return ExtensionsMetricsProvider::GetInstallsForProfileForTesting( |
| profile(), last_sample_time_); |
| } |
| |
| extensions::ExtensionPrefs* prefs() { return prefs_; } |
| void set_last_sample_time(base::Time last_sample_time) { |
| last_sample_time_ = last_sample_time; |
| } |
| |
| private: |
| raw_ptr<extensions::ExtensionPrefs> prefs_ = nullptr; |
| base::Time last_sample_time_; |
| }; |
| |
| // Tests the various aspects of constructing a relevant proto for a given |
| // extension installation. |
| TEST_F(ExtensionMetricsProviderInstallsTest, TestProtoConstruction) { |
| auto add_extension = [this](const Extension* extension) { |
| prefs()->OnExtensionInstalled(extension, Extension::ENABLED, |
| syncer::StringOrdinal(), std::string()); |
| }; |
| |
| { |
| // Test basic prototype construction. All fields should be present, except |
| // disable reasons (which should be empty). |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("test") |
| .SetLocation(ManifestLocation::kInternal) |
| .Build(); |
| add_extension(extension.get()); |
| ExtensionInstallProto install = ConstructProto(*extension); |
| EXPECT_TRUE(install.has_type()); |
| EXPECT_EQ(ExtensionInstallProto::EXTENSION, install.type()); |
| |
| EXPECT_TRUE(install.has_install_location()); |
| EXPECT_EQ(ExtensionInstallProto::INTERNAL, install.install_location()); |
| |
| EXPECT_TRUE(install.has_manifest_version()); |
| EXPECT_EQ(2, install.manifest_version()); |
| |
| EXPECT_TRUE(install.has_action_type()); |
| EXPECT_EQ(ExtensionInstallProto::NO_ACTION, install.action_type()); |
| |
| EXPECT_TRUE(install.has_has_file_access()); |
| EXPECT_FALSE(install.has_file_access()); |
| |
| EXPECT_TRUE(install.has_has_incognito_access()); |
| EXPECT_FALSE(install.has_incognito_access()); |
| |
| EXPECT_TRUE(install.has_updates_from_store()); |
| EXPECT_FALSE(install.updates_from_store()); |
| |
| EXPECT_TRUE(install.has_is_from_bookmark()); |
| EXPECT_FALSE(install.is_from_bookmark()); |
| |
| EXPECT_TRUE(install.has_is_converted_from_user_script()); |
| EXPECT_FALSE(install.is_converted_from_user_script()); |
| |
| EXPECT_TRUE(install.has_is_default_installed()); |
| EXPECT_FALSE(install.is_default_installed()); |
| |
| EXPECT_TRUE(install.has_is_oem_installed()); |
| EXPECT_FALSE(install.is_oem_installed()); |
| |
| EXPECT_TRUE(install.has_background_script_type()); |
| EXPECT_EQ(ExtensionInstallProto::NO_BACKGROUND_SCRIPT, |
| install.background_script_type()); |
| |
| EXPECT_EQ(0, install.disable_reasons_size()); |
| |
| EXPECT_TRUE(install.has_blacklist_state()); |
| EXPECT_EQ(ExtensionInstallProto::NOT_BLACKLISTED, |
| install.blacklist_state()); |
| |
| EXPECT_TRUE(install.has_installed_in_this_sample_period()); |
| EXPECT_TRUE(install.installed_in_this_sample_period()); |
| } |
| |
| // It's not helpful to exhaustively test each possible variation of each |
| // field in the proto (since in many cases the test code would then be |
| // re-writing the original code), but we test a few of the more interesting |
| // cases. |
| |
| { |
| // Test the type() field; extensions of different types should be reported |
| // as such. |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("app", ExtensionBuilder::Type::PLATFORM_APP) |
| .SetLocation(ManifestLocation::kInternal) |
| .Build(); |
| add_extension(extension.get()); |
| ExtensionInstallProto install = ConstructProto(*extension); |
| EXPECT_EQ(ExtensionInstallProto::PLATFORM_APP, install.type()); |
| } |
| |
| { |
| // Test the install location. |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("unpacked") |
| .SetLocation(ManifestLocation::kUnpacked) |
| .Build(); |
| add_extension(extension.get()); |
| ExtensionInstallProto install = ConstructProto(*extension); |
| EXPECT_EQ(ExtensionInstallProto::UNPACKED, install.install_location()); |
| } |
| |
| { |
| // Test the extension action as a browser action. |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("browser_action") |
| .SetLocation(ManifestLocation::kInternal) |
| .SetAction(extensions::ActionInfo::Type::kBrowser) |
| .Build(); |
| add_extension(extension.get()); |
| ExtensionInstallProto install = ConstructProto(*extension); |
| EXPECT_EQ(ExtensionInstallProto::BROWSER_ACTION, install.action_type()); |
| } |
| |
| { |
| // Test the extension action as a page action. |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("page_action") |
| .SetLocation(ManifestLocation::kInternal) |
| .SetAction(extensions::ActionInfo::Type::kPage) |
| .Build(); |
| add_extension(extension.get()); |
| ExtensionInstallProto install = ConstructProto(*extension); |
| EXPECT_EQ(ExtensionInstallProto::PAGE_ACTION, install.action_type()); |
| } |
| |
| { |
| // Test the disable reasons field. |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("disable_reasons") |
| .SetLocation(ManifestLocation::kInternal) |
| .Build(); |
| add_extension(extension.get()); |
| prefs()->SetExtensionDisabled( |
| extension->id(), extensions::disable_reason::DISABLE_USER_ACTION); |
| { |
| ExtensionInstallProto install = ConstructProto(*extension); |
| ASSERT_EQ(1, install.disable_reasons_size()); |
| EXPECT_EQ(ExtensionInstallProto::USER_ACTION, |
| install.disable_reasons().Get(0)); |
| } |
| // Adding additional disable reasons should result in all reasons being |
| // reported. |
| prefs()->AddDisableReason(extension->id(), |
| extensions::disable_reason::DISABLE_CORRUPTED); |
| { |
| ExtensionInstallProto install = ConstructProto(*extension); |
| ASSERT_EQ(2, install.disable_reasons_size()); |
| EXPECT_EQ(ExtensionInstallProto::USER_ACTION, |
| install.disable_reasons().Get(0)); |
| EXPECT_EQ(ExtensionInstallProto::CORRUPTED, |
| install.disable_reasons().Get(1)); |
| } |
| } |
| |
| { |
| // Test that event pages are reported correctly. |
| auto background = |
| base::Value::Dict() |
| .Set("persistent", false) |
| .Set("scripts", base::Value::List().Append("script.js")); |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("event_page") |
| .SetLocation(ManifestLocation::kInternal) |
| .MergeManifest( |
| base::Value::Dict().Set("background", std::move(background))) |
| .Build(); |
| add_extension(extension.get()); |
| ExtensionInstallProto install = ConstructProto(*extension); |
| EXPECT_EQ(ExtensionInstallProto::EVENT_PAGE, |
| install.background_script_type()); |
| } |
| |
| { |
| // Test that persistent background pages are reported correctly. |
| auto background = |
| base::Value::Dict() |
| .Set("persistent", true) |
| .Set("scripts", base::Value::List().Append("script.js")); |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("persisent_background") |
| .SetLocation(ManifestLocation::kInternal) |
| .MergeManifest( |
| base::Value::Dict().Set("background", std::move(background))) |
| .Build(); |
| add_extension(extension.get()); |
| ExtensionInstallProto install = ConstructProto(*extension); |
| EXPECT_EQ(ExtensionInstallProto::PERSISTENT_BACKGROUND_PAGE, |
| install.background_script_type()); |
| } |
| { |
| // Test that service worker scripts are reported correctly. |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("service worker") |
| .SetBackgroundContext( |
| ExtensionBuilder::BackgroundContext::SERVICE_WORKER) |
| .Build(); |
| add_extension(extension.get()); |
| ExtensionInstallProto install = ConstructProto(*extension); |
| EXPECT_EQ(ExtensionInstallProto::SERVICE_WORKER, |
| install.background_script_type()); |
| } |
| |
| { |
| // Test changing the blacklist state. |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("blacklist") |
| .SetLocation(ManifestLocation::kInternal) |
| .Build(); |
| add_extension(extension.get()); |
| extensions::blocklist_prefs::SetSafeBrowsingExtensionBlocklistState( |
| extension->id(), |
| extensions::BitMapBlocklistState::BLOCKLISTED_SECURITY_VULNERABILITY, |
| prefs()); |
| ExtensionInstallProto install = ConstructProto(*extension); |
| EXPECT_EQ(ExtensionInstallProto::BLACKLISTED_SECURITY_VULNERABILITY, |
| install.blacklist_state()); |
| } |
| |
| { |
| // Test that the installed_in_this_sample_period boolean is correctly |
| // reported. |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("installtime") |
| .SetLocation(ManifestLocation::kInternal) |
| .Build(); |
| add_extension(extension.get()); |
| set_last_sample_time(base::Time::Now() + base::Minutes(60)); |
| ExtensionInstallProto install = ConstructProto(*extension); |
| EXPECT_FALSE(install.installed_in_this_sample_period()); |
| } |
| } |
| |
| // Tests that we retrieve all extensions associated with a given profile. |
| TEST_F(ExtensionMetricsProviderInstallsTest, |
| TestGettingAllExtensionsInProfile) { |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("extension").Build(); |
| service()->AddExtension(extension.get()); |
| scoped_refptr<const Extension> app = |
| ExtensionBuilder("app", ExtensionBuilder::Type::PLATFORM_APP).Build(); |
| service()->AddExtension(app.get()); |
| service()->DisableExtension(app->id(), |
| extensions::disable_reason::DISABLE_USER_ACTION); |
| |
| std::vector<ExtensionInstallProto> installs = GetInstallsForProfile(); |
| // There should be two installs total. |
| ASSERT_EQ(2u, installs.size()); |
| // One should be the extension, and the other should be the app. We don't |
| // check the specifics of the proto, since that's tested above. |
| EXPECT_TRUE(base::Contains(installs, ExtensionInstallProto::EXTENSION, |
| &ExtensionInstallProto::type)); |
| EXPECT_TRUE(base::Contains(installs, ExtensionInstallProto::PLATFORM_APP, |
| &ExtensionInstallProto::type)); |
| } |