|  | // Copyright 2023 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/structured/metadata_processor_ash.h" | 
|  |  | 
|  | #include <optional> | 
|  | #include <utility> | 
|  |  | 
|  | #include "ash/constants/ash_switches.h" | 
|  | #include "ash/public/cpp/login_screen_test_api.h" | 
|  | #include "base/feature_list.h" | 
|  | #include "base/logging.h" | 
|  | #include "base/run_loop.h" | 
|  | #include "base/test/metrics/histogram_tester.h" | 
|  | #include "build/build_config.h" | 
|  | #include "chrome/browser/ash/app_mode/kiosk_test_helper.h" | 
|  | #include "chrome/browser/ash/app_mode/test/kiosk_test_utils.h" | 
|  | #include "chrome/browser/ash/app_mode/test/scoped_device_settings.h" | 
|  | #include "chrome/browser/ash/app_mode/web_app/kiosk_web_app_data.h" | 
|  | #include "chrome/browser/ash/app_mode/web_app/kiosk_web_app_manager.h" | 
|  | #include "chrome/browser/ash/login/demo_mode/demo_mode_test_utils.h" | 
|  | #include "chrome/browser/ash/login/demo_mode/demo_session.h" | 
|  | #include "chrome/browser/ash/login/existing_user_controller.h" | 
|  | #include "chrome/browser/ash/login/test/logged_in_user_mixin.h" | 
|  | #include "chrome/browser/ash/login/test/session_manager_state_waiter.h" | 
|  | #include "chrome/browser/ash/login/wizard_controller.h" | 
|  | #include "chrome/browser/ash/ownership/fake_owner_settings_service.h" | 
|  | #include "chrome/browser/ash/policy/core/device_local_account.h" | 
|  | #include "chrome/browser/ash/policy/core/device_policy_cros_browser_test.h" | 
|  | #include "chrome/browser/ash/policy/test_support/embedded_policy_test_server_mixin.h" | 
|  | #include "chrome/browser/browser_process.h" | 
|  | #include "chrome/browser/browser_process_platform_part_ash.h" | 
|  | #include "chrome/browser/metrics/structured/test/structured_metrics_mixin.h" | 
|  | #include "chrome/test/base/fake_gaia_mixin.h" | 
|  | #include "chromeos/ash/components/dbus/session_manager/fake_session_manager_client.h" | 
|  | #include "chromeos/ash/components/policy/device_local_account/device_local_account_type.h" | 
|  | #include "components/metrics/metrics_service.h" | 
|  | #include "components/metrics/structured/structured_events.h" | 
|  | #include "components/metrics/structured/structured_metrics_client.h" | 
|  | #include "components/metrics/structured/structured_metrics_features.h" | 
|  | #include "components/metrics/structured/structured_metrics_service.h" | 
|  | #include "components/policy/core/common/cloud/cloud_policy_constants.h" | 
|  | #include "components/policy/core/common/cloud/mock_cloud_policy_store.h" | 
|  | #include "components/policy/core/common/cloud/test/policy_builder.h" | 
|  | #include "components/policy/proto/device_management_backend.pb.h" | 
|  | #include "content/public/test/browser_test.h" | 
|  | #include "content/public/test/browser_test_utils.h" | 
|  |  | 
|  | namespace { | 
|  | namespace em = enterprise_management; | 
|  | using ash::KioskWebAppManager; | 
|  | using ash::LoginScreenTestApi; | 
|  | using ash::ScopedDeviceSettings; | 
|  | using ash::kiosk::test::WaitKioskLaunched; | 
|  | using testing::InvokeWithoutArgs; | 
|  | using StructuredEventProto = metrics::StructuredEventProto; | 
|  | using StructuredDataProto = metrics::StructuredDataProto; | 
|  | using PrimaryUserSegment = StructuredEventProto::PrimaryUserSegment; | 
|  | using DeviceSegment = StructuredDataProto::DeviceSegment; | 
|  |  | 
|  | const char kAccountId1[] = "dla1@example.com"; | 
|  | const char kDisplayName1[] = "display name 1"; | 
|  | const char kAppInstallUrl[] = "https://app.com/install"; | 
|  |  | 
|  | // The name hash of "CrOSEvents". | 
|  | constexpr uint64_t kCrosEventsHash = UINT64_C(12657197978410187837); | 
|  |  | 
|  | // The name hash of "chrome::CrOSEvents::NoMetricsEvent". | 
|  | constexpr uint64_t kNoMetricsEventHash = UINT64_C(5106854608989380457); | 
|  |  | 
|  | std::optional<em::PolicyData::MarketSegment> GetMarketSegment( | 
|  | DeviceSegment device_segment) { | 
|  | switch (device_segment) { | 
|  | case StructuredDataProto::UNKNOWN: | 
|  | case StructuredDataProto::CONSUMER: | 
|  | return std::nullopt; | 
|  | case StructuredDataProto::EDUCATION: | 
|  | return em::PolicyData::ENROLLED_EDUCATION; | 
|  | case StructuredDataProto::ENTERPRISE: | 
|  | return em::PolicyData::ENROLLED_ENTERPRISE; | 
|  | } | 
|  | NOTREACHED(); | 
|  | } | 
|  |  | 
|  | std::optional<em::PolicyData::MetricsLogSegment> GetMetricsLogSegment( | 
|  | PrimaryUserSegment primary_user_segment) { | 
|  | switch (primary_user_segment) { | 
|  | case StructuredEventProto::K12: | 
|  | return em::PolicyData::K12; | 
|  | case StructuredEventProto::UNIVERSITY: | 
|  | return em::PolicyData::UNIVERSITY; | 
|  | case StructuredEventProto::NON_PROFIT: | 
|  | return em::PolicyData::NONPROFIT; | 
|  | case StructuredEventProto::ENTERPRISE_ORGANIZATION: | 
|  | return em::PolicyData::ENTERPRISE; | 
|  | case StructuredEventProto::UNMANAGED: | 
|  | case StructuredEventProto::KIOS_APP: | 
|  | case StructuredEventProto::MANAGED_GUEST_SESSION: | 
|  | case StructuredEventProto::DEMO_MODE: | 
|  | case StructuredEventProto::UNKNOWN_PRIMARY_USER_TYPE: | 
|  | return std::nullopt; | 
|  | } | 
|  | NOTREACHED(); | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | namespace metrics::structured { | 
|  |  | 
|  | namespace { | 
|  | class TestCase { | 
|  | public: | 
|  | TestCase(PrimaryUserSegment primary_user_segment, | 
|  | DeviceSegment device_segment) | 
|  | : primary_user_segment_(primary_user_segment), | 
|  | device_segment_(device_segment) {} | 
|  |  | 
|  | PrimaryUserSegment GetPrimaryUserSegement() const { | 
|  | return primary_user_segment_; | 
|  | } | 
|  |  | 
|  | DeviceSegment GetDeviceSegment() const { return device_segment_; } | 
|  |  | 
|  | std::optional<em::PolicyData::MetricsLogSegment> GetMetricsLogSegment() | 
|  | const { | 
|  | return ::GetMetricsLogSegment(primary_user_segment_); | 
|  | } | 
|  |  | 
|  | std::optional<em::PolicyData::MarketSegment> GetMarketSegment() const { | 
|  | return ::GetMarketSegment(device_segment_); | 
|  | } | 
|  |  | 
|  | bool IsPublicSession() const { | 
|  | return primary_user_segment_ == StructuredEventProto::MANAGED_GUEST_SESSION; | 
|  | } | 
|  |  | 
|  | bool IsKioskApp() const { | 
|  | return primary_user_segment_ == StructuredEventProto::KIOS_APP; | 
|  | } | 
|  |  | 
|  | bool IsDemoSession() const { | 
|  | return primary_user_segment_ == StructuredEventProto::DEMO_MODE; | 
|  | } | 
|  |  | 
|  | private: | 
|  | PrimaryUserSegment primary_user_segment_; | 
|  | DeviceSegment device_segment_; | 
|  | }; | 
|  |  | 
|  | TestCase UserCase(PrimaryUserSegment user_segment, | 
|  | DeviceSegment device_segment) { | 
|  | TestCase test_case(user_segment, device_segment); | 
|  | return test_case; | 
|  | } | 
|  |  | 
|  | TestCase MgsCase(DeviceSegment device_segment) { | 
|  | TestCase test_case(StructuredEventProto::MANAGED_GUEST_SESSION, | 
|  | device_segment); | 
|  | return test_case; | 
|  | } | 
|  |  | 
|  | TestCase KioskCase(DeviceSegment device_segment) { | 
|  | TestCase test_case(StructuredEventProto::KIOS_APP, device_segment); | 
|  | return test_case; | 
|  | } | 
|  |  | 
|  | TestCase DemoModeCase() { | 
|  | TestCase test_case(StructuredEventProto::DEMO_MODE, | 
|  | StructuredDataProto::ENTERPRISE); | 
|  | return test_case; | 
|  | } | 
|  | }  // namespace | 
|  |  | 
|  | class MetadataProcessorTest : public policy::DevicePolicyCrosBrowserTest, | 
|  | public testing::WithParamInterface<TestCase> { | 
|  | public: | 
|  | MetadataProcessorTest() { | 
|  | if (GetParam().IsDemoSession()) { | 
|  | device_state_.SetState( | 
|  | ash::DeviceStateMixin::State::OOBE_COMPLETED_DEMO_MODE); | 
|  | } | 
|  | } | 
|  |  | 
|  | void SetUpInProcessBrowserTestFixture() override { | 
|  | policy::DevicePolicyCrosBrowserTest::SetUpInProcessBrowserTestFixture(); | 
|  | InitializePolicy(); | 
|  | } | 
|  |  | 
|  | void SetUpCommandLine(base::CommandLine* command_line) override { | 
|  | command_line->AppendSwitch(ash::switches::kOobeSkipPostLogin); | 
|  | DevicePolicyCrosBrowserTest::SetUpCommandLine(command_line); | 
|  | } | 
|  |  | 
|  | void TearDownOnMainThread() override { | 
|  | settings_.reset(); | 
|  | policy::DevicePolicyCrosBrowserTest::TearDownOnMainThread(); | 
|  | } | 
|  |  | 
|  | protected: | 
|  | void InitializePolicy() { | 
|  | device_policy()->policy_data().set_public_key_version(1); | 
|  | policy::DeviceLocalAccountTestHelper::SetupDeviceLocalAccount( | 
|  | &device_local_account_policy_, kAccountId1, kDisplayName1); | 
|  | } | 
|  |  | 
|  | void BuildDeviceLocalAccountPolicy() { | 
|  | device_local_account_policy_.SetDefaultSigningKey(); | 
|  | device_local_account_policy_.Build(); | 
|  | } | 
|  |  | 
|  | void UploadDeviceLocalAccountPolicy() { | 
|  | BuildDeviceLocalAccountPolicy(); | 
|  | logged_in_user_mixin_.GetEmbeddedPolicyTestServerMixin() | 
|  | ->UpdateExternalPolicy( | 
|  | policy::dm_protocol::kChromePublicAccountPolicyType, kAccountId1, | 
|  | device_local_account_policy_.payload().SerializeAsString()); | 
|  | } | 
|  |  | 
|  | void UploadAndInstallDeviceLocalAccountPolicy() { | 
|  | UploadDeviceLocalAccountPolicy(); | 
|  | session_manager_client()->set_device_local_account_policy( | 
|  | kAccountId1, device_local_account_policy_.GetBlob()); | 
|  | } | 
|  |  | 
|  | void SetDevicePolicy() { | 
|  | UploadAndInstallDeviceLocalAccountPolicy(); | 
|  | // Add an account with DeviceLocalAccountType::kPublicSession. | 
|  | AddPublicSessionToDevicePolicy(kAccountId1); | 
|  |  | 
|  | std::optional<em::PolicyData::MarketSegment> market_segment = | 
|  | GetParam().GetMarketSegment(); | 
|  | if (market_segment) { | 
|  | device_policy()->policy_data().set_market_segment(market_segment.value()); | 
|  | RefreshDevicePolicy(); | 
|  | } | 
|  | WaitForPolicy(); | 
|  | } | 
|  |  | 
|  | void AddPublicSessionToDevicePolicy(const std::string& username) { | 
|  | em::ChromeDeviceSettingsProto& proto(device_policy()->payload()); | 
|  | policy::DeviceLocalAccountTestHelper::AddPublicSession(&proto, username); | 
|  | RefreshDevicePolicy(); | 
|  | logged_in_user_mixin_.GetEmbeddedPolicyTestServerMixin() | 
|  | ->UpdateDevicePolicy(proto); | 
|  | } | 
|  |  | 
|  | void WaitForDisplayName(const std::string& user_id, | 
|  | const std::string& expected_display_name) { | 
|  | policy::DictionaryLocalStateValueWaiter("UserDisplayName", | 
|  | expected_display_name, user_id) | 
|  | .Wait(); | 
|  | } | 
|  |  | 
|  | void WaitForPolicy() { | 
|  | // Wait for the display name becoming available as that indicates | 
|  | // device-local account policy is fully loaded, which is a prerequisite for | 
|  | // successful login. | 
|  | WaitForDisplayName(account_id_1_.GetUserEmail(), kDisplayName1); | 
|  | } | 
|  |  | 
|  | void LogInUser() { | 
|  | std::optional<em::PolicyData::MetricsLogSegment> log_segment = | 
|  | GetParam().GetMetricsLogSegment(); | 
|  | if (log_segment) { | 
|  | logged_in_user_mixin_.GetEmbeddedPolicyTestServerMixin() | 
|  | ->SetMetricsLogSegment(log_segment.value()); | 
|  | } | 
|  | logged_in_user_mixin_.LogInUser(); | 
|  | } | 
|  |  | 
|  | void StartPublicSession() { | 
|  | StartPublicSessionLogin(); | 
|  | WaitForSessionStart(); | 
|  | } | 
|  |  | 
|  | void StartPublicSessionLogin() { | 
|  | // Start login into the device-local account. | 
|  | auto* host = ash::LoginDisplayHost::default_host(); | 
|  | ASSERT_TRUE(host); | 
|  | host->StartSignInScreen(); | 
|  | auto* controller = ash::ExistingUserController::current_controller(); | 
|  | ASSERT_TRUE(controller); | 
|  |  | 
|  | ash::UserContext user_context(user_manager::UserType::kPublicAccount, | 
|  | account_id_1_); | 
|  | controller->Login(user_context, ash::SigninSpecifics()); | 
|  | } | 
|  |  | 
|  | void StartDemoSession() { | 
|  | // Set Demo Mode config to online. | 
|  | ash::DemoSession::SetDemoConfigForTesting( | 
|  | ash::DemoSession::DemoModeConfig::kOnline); | 
|  | ash::test::LockDemoDeviceInstallAttributes(); | 
|  | ash::DemoSession::StartIfInDemoMode(); | 
|  |  | 
|  | // Start the public session, Demo Mode is a special public session. | 
|  | StartPublicSession(); | 
|  | } | 
|  |  | 
|  | void PrepareAppLaunch() { | 
|  | std::vector<policy::DeviceLocalAccount> device_local_accounts = { | 
|  | policy::DeviceLocalAccount( | 
|  | policy::DeviceLocalAccount::EphemeralMode::kUnset, | 
|  | policy::WebKioskAppBasicInfo(kAppInstallUrl, "", ""), | 
|  | kAppInstallUrl)}; | 
|  |  | 
|  | settings_ = std::make_unique<ScopedDeviceSettings>(); | 
|  | int ui_update_count = LoginScreenTestApi::GetUiUpdateCount(); | 
|  | policy::SetDeviceLocalAccountsForTesting( | 
|  | settings_->owner_settings_service(), device_local_accounts); | 
|  | // Wait for the Kiosk App configuration to reload. | 
|  | LoginScreenTestApi::WaitForUiUpdate(ui_update_count); | 
|  | } | 
|  |  | 
|  | bool LaunchApp() { | 
|  | return LoginScreenTestApi::LaunchApp( | 
|  | KioskWebAppManager::Get()->GetAppByAccountId(account_id_2_)->app_id()); | 
|  | } | 
|  |  | 
|  | void StartKioskApp() { | 
|  | PrepareAppLaunch(); | 
|  | LaunchApp(); | 
|  | ASSERT_TRUE(WaitKioskLaunched()); | 
|  | } | 
|  |  | 
|  | void WaitForSessionStart() { | 
|  | if (IsSessionStarted()) { | 
|  | return; | 
|  | } | 
|  | ash::test::WaitForPrimaryUserSessionStart(); | 
|  | } | 
|  |  | 
|  | bool IsSessionStarted() { | 
|  | return session_manager::SessionManager::Get()->IsSessionStarted(); | 
|  | } | 
|  |  | 
|  | void Wait() { base::RunLoop().RunUntilIdle(); } | 
|  |  | 
|  | protected: | 
|  | StructuredMetricsMixin structured_metrics_mixin_{&mixin_host_, | 
|  | /*setup_profile=*/false}; | 
|  |  | 
|  | // Helper function to find an event in an already build Uma Proto. | 
|  | static std::optional<StructuredEventProto> FindEvent( | 
|  | const StructuredDataProto& structured_data, | 
|  | uint64_t project_name_hash, | 
|  | uint64_t event_name_hash) { | 
|  | for (const auto& event : structured_data.events()) { | 
|  | if (event.project_name_hash() == project_name_hash && | 
|  | event.event_name_hash() == event_name_hash) { | 
|  | return event; | 
|  | } | 
|  | } | 
|  | return std::nullopt; | 
|  | } | 
|  |  | 
|  | private: | 
|  | ash::LoggedInUserMixin logged_in_user_mixin_{ | 
|  | &mixin_host_, /*test_base=*/this, embedded_test_server(), | 
|  | ash::LoggedInUserMixin::LogInType::kManaged}; | 
|  | policy::UserPolicyBuilder device_local_account_policy_; | 
|  |  | 
|  | const AccountId account_id_1_ = | 
|  | AccountId::FromUserEmail(policy::GenerateDeviceLocalAccountUserId( | 
|  | kAccountId1, | 
|  | policy::DeviceLocalAccountType::kPublicSession)); | 
|  | const AccountId account_id_2_ = | 
|  | AccountId::FromUserEmail(policy::GenerateDeviceLocalAccountUserId( | 
|  | kAppInstallUrl, | 
|  | policy::DeviceLocalAccountType::kWebKioskApp)); | 
|  | // Not strictly necessary, but makes kiosk tests run much faster. | 
|  | base::AutoReset<bool> skip_splash_wait_override_ = | 
|  | ash::KioskTestHelper::SkipSplashScreenWait(); | 
|  | std::unique_ptr<ScopedDeviceSettings> settings_; | 
|  | }; | 
|  |  | 
|  | // TODO(b/303139902) re-enable test once flakiness is addressed. | 
|  | IN_PROC_BROWSER_TEST_P(MetadataProcessorTest, DISABLED_UserMetadata) { | 
|  | using NoMetricsEvent = events::v2::cr_os_events::NoMetricsEvent; | 
|  |  | 
|  | SetDevicePolicy(); | 
|  |  | 
|  | if (GetParam().IsPublicSession()) { | 
|  | StartPublicSession(); | 
|  | } else if (GetParam().IsKioskApp()) { | 
|  | StartKioskApp(); | 
|  | } else if (GetParam().IsDemoSession()) { | 
|  | StartDemoSession(); | 
|  | } else { | 
|  | LogInUser(); | 
|  | } | 
|  |  | 
|  | structured_metrics_mixin_.UpdateRecordingState(true); | 
|  |  | 
|  | structured_metrics_mixin_.WaitUntilKeysReady(); | 
|  |  | 
|  | ASSERT_TRUE(structured_metrics_mixin_.GetRecorder()->CanProvideMetrics()); | 
|  |  | 
|  | StructuredMetricsClient::Record(std::move(NoMetricsEvent())); | 
|  |  | 
|  | Wait(); | 
|  |  | 
|  | structured_metrics_mixin_.WaitUntilEventRecorded(kCrosEventsHash, | 
|  | kNoMetricsEventHash); | 
|  |  | 
|  | structured_metrics_mixin_.GetService()->Flush( | 
|  | metrics::MetricsLogsEventManager::CreateReason::kUnknown); | 
|  |  | 
|  | std::unique_ptr<ChromeUserMetricsExtension> uma_proto = | 
|  | structured_metrics_mixin_.GetUmaProto(); | 
|  | ASSERT_NE(uma_proto.get(), nullptr); | 
|  |  | 
|  | const StructuredDataProto& structured_data = uma_proto->structured_data(); | 
|  | EXPECT_EQ(structured_data.device_segment(), GetParam().GetDeviceSegment()); | 
|  |  | 
|  | std::optional<StructuredEventProto> event = | 
|  | FindEvent(structured_data, kCrosEventsHash, kNoMetricsEventHash); | 
|  | ASSERT_TRUE(event.has_value()); | 
|  |  | 
|  | EXPECT_EQ(event->event_sequence_metadata().primary_user_segment(), | 
|  | GetParam().GetPrimaryUserSegement()); | 
|  | } | 
|  |  | 
|  | INSTANTIATE_TEST_SUITE_P( | 
|  | , | 
|  | MetadataProcessorTest, | 
|  | testing::Values( | 
|  | UserCase(StructuredEventProto::UNMANAGED, | 
|  | StructuredDataProto::CONSUMER), | 
|  | UserCase(StructuredEventProto::NON_PROFIT, | 
|  | StructuredDataProto::CONSUMER), | 
|  | UserCase(StructuredEventProto::UNIVERSITY, | 
|  | StructuredDataProto::EDUCATION), | 
|  | UserCase(StructuredEventProto::NON_PROFIT, | 
|  | StructuredDataProto::EDUCATION), | 
|  | UserCase(StructuredEventProto::K12, StructuredDataProto::ENTERPRISE), | 
|  | UserCase(StructuredEventProto::ENTERPRISE_ORGANIZATION, | 
|  | StructuredDataProto::ENTERPRISE), | 
|  | metrics::structured::KioskCase(StructuredDataProto::ENTERPRISE), | 
|  | MgsCase(StructuredDataProto::CONSUMER), | 
|  | DemoModeCase())); | 
|  |  | 
|  | }  // namespace metrics::structured |