chromium/chrome/browser/ash/policy/reporting/metrics_reporting/apps/app_usage_telemetry_sampler_browsertest.cc

// 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 <memory>
#include <string>
#include <string_view>
#include <tuple>
#include <vector>

#include "base/check.h"
#include "base/functional/bind.h"
#include "base/time/time.h"
#include "base/time/time_override.h"
#include "chrome/browser/apps/app_service/metrics/app_platform_metrics.h"
#include "chrome/browser/apps/app_service/metrics/app_platform_metrics_utils.h"
#include "chrome/browser/ash/login/test/cryptohome_mixin.h"
#include "chrome/browser/ash/policy/affiliation/affiliation_mixin.h"
#include "chrome/browser/ash/policy/affiliation/affiliation_test_helper.h"
#include "chrome/browser/ash/policy/core/device_policy_cros_browser_test.h"
#include "chrome/browser/ash/policy/reporting/metrics_reporting/metric_reporting_prefs.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/chromeos/reporting/metric_default_utils.h"
#include "chrome/browser/policy/dm_token_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sync/sync_service_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/web_applications/mojom/user_display_mode.mojom-shared.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/user_display_mode.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chromeos/ash/components/login/session/session_termination_manager.h"
#include "chromeos/dbus/missive/missive_client_test_observer.h"
#include "components/app_constants/constants.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "components/keyed_service/core/keyed_service.h"
#include "components/reporting/proto/synced/metric_data.pb.h"
#include "components/reporting/proto/synced/record.pb.h"
#include "components/reporting/proto/synced/record_constants.pb.h"
#include "components/reporting/util/mock_clock.h"
#include "components/services/app_service/public/protos/app_types.pb.h"
#include "components/sync/test/test_sync_service.h"
#include "components/ukm/test_ukm_recorder.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/browser_context.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/test_launcher.h"
#include "content/public/test/test_utils.h"
#include "services/metrics/public/cpp/ukm_source.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/manifest/display_mode.mojom-shared.h"
#include "third_party/cros_system_api/dbus/login_manager/dbus-constants.h"
#include "url/gurl.h"

using ::testing::AllOf;
using ::testing::Eq;
using ::testing::Ge;
using ::testing::Le;
using ::testing::StrEq;

namespace reporting {
namespace {

// Test DM token used to associate reported events.
constexpr char kDMToken[] = "token";

// Standalone webapp start URL.
constexpr char kWebAppUrl[] = "https://test.example.com/";

// App usage UKM entry name.
constexpr char kAppUsageUKMEntryName[] = "ChromeOSApp.UsageTime";

// App usage collection interval.
constexpr base::TimeDelta kAppUsageCollectionInterval = base::Minutes(5);

// UKM app usage reporting interval.
constexpr base::TimeDelta kAppUsageUKMReportingInterval = base::Hours(2);

// Additional webapp usage buffer period before the browser is actually closed.
// Used when validating reported app usage data.
constexpr base::TimeDelta kWebAppUsageBufferPeriod = base::Seconds(10);

void AssertRecordData(Priority priority, const Record& record) {
  EXPECT_THAT(priority, Eq(Priority::MANUAL_BATCH));
  ASSERT_TRUE(record.has_destination());
  EXPECT_THAT(record.destination(), Eq(Destination::TELEMETRY_METRIC));
  ASSERT_TRUE(record.has_dm_token());
  EXPECT_THAT(record.dm_token(), StrEq(kDMToken));
  ASSERT_TRUE(record.has_source_info());
  EXPECT_THAT(record.source_info().source(), Eq(SourceInfo::ASH));
}

// Returns true if the record includes app usage telemetry. False otherwise.
bool IsAppUsageTelemetry(const Record& record) {
  MetricData record_data;
  return record_data.ParseFromString(record.data()) &&
         record_data.has_telemetry_data() &&
         record_data.telemetry_data().has_app_telemetry() &&
         record_data.telemetry_data().app_telemetry().has_app_usage_data();
}

// Browser test that validates app usage telemetry reported by the
// `AppUsageTelemetrySampler`. Inheriting from `DevicePolicyCrosBrowserTest`
// enables use of `AffiliationMixin` for setting up profile/device affiliation.
// Only available in Ash.
class AppUsageTelemetrySamplerBrowserTest
    : public ::policy::DevicePolicyCrosBrowserTest {
 protected:
  AppUsageTelemetrySamplerBrowserTest() {
    // Initialize the MockClock.
    test::MockClock::Get();
    crypto_home_mixin_.MarkUserAsExisting(affiliation_mixin_.account_id());
    ::policy::SetDMTokenForTesting(
        ::policy::DMToken::CreateValidToken(kDMToken));
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    ::policy::AffiliationTestHelper::AppendCommandLineSwitchesForLoginManager(
        command_line);
    ::policy::DevicePolicyCrosBrowserTest::SetUpCommandLine(command_line);
  }

  void SetUpOnMainThread() override {
    ::policy::DevicePolicyCrosBrowserTest::SetUpOnMainThread();
    if (::content::IsPreTest()) {
      // Preliminary setup - set up affiliated user.
      ::policy::AffiliationTestHelper::PreLoginUser(
          affiliation_mixin_.account_id());
      return;
    }

    // Login as affiliated user otherwise and set up test environment.
    ::policy::AffiliationTestHelper::LoginUser(affiliation_mixin_.account_id());
    ::web_app::test::UninstallAllWebApps(profile());
    SetAllowedAppReportingTypes({::ash::reporting::kAppCategoryPWA});
    test_ukm_recorder_ = std::make_unique<::ukm::TestAutoSetUkmRecorder>();
  }

  void SetUpInProcessBrowserTestFixture() override {
    ::policy::DevicePolicyCrosBrowserTest::SetUpInProcessBrowserTestFixture();
    create_sync_service_subscription_ =
        BrowserContextDependencyManager::GetInstance()
            ->RegisterCreateServicesCallbackForTesting(base::BindRepeating(
                &AppUsageTelemetrySamplerBrowserTest::SetUpSyncService,
                base::Unretained(this)));
  }

  void SetUpSyncService(::content::BrowserContext* context) {
    SyncServiceFactory::GetInstance()->SetTestingFactoryAndUse(
        context, base::BindRepeating([](::content::BrowserContext* context)
                                         -> std::unique_ptr<KeyedService> {
          return std::make_unique<::syncer::TestSyncService>();
        }));
  }

  // Helper that installs a standalone webapp with the specified start url.
  ::webapps::AppId InstallStandaloneWebApp(const GURL& start_url) {
    auto web_app_info =
        web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(start_url);
    web_app_info->scope = start_url.GetWithoutFilename();
    web_app_info->display_mode = ::blink::mojom::DisplayMode::kStandalone;
    web_app_info->user_display_mode =
        ::web_app::mojom::UserDisplayMode::kStandalone;
    return ::web_app::test::InstallWebApp(profile(), std::move(web_app_info));
  }

  // Helper that simulates app usage for the specified app and usage duration.
  void SimulateAppUsage(const ::webapps::AppId& app_id,
                        const base::TimeDelta& running_time) {
    // Launch web app and simulate web app usage before closing the browser
    // window to prevent further usage tracking.
    Browser* const app_browser =
        ::web_app::LaunchWebAppBrowser(profile(), app_id);
    test::MockClock::Get().Advance(running_time);
    ::web_app::CloseAndWait(app_browser);

    // Trigger usage telemetry collection by advancing the clock. Wait before
    // returning to ensure usage data gets persisted in the user pref store.
    test::MockClock::Get().Advance(kAppUsageCollectionInterval);
    ::content::RunAllTasksUntilIdle();
  }

  void VerifyAppUsage(const AppUsageData::AppUsage& app_usage,
                      const base::TimeDelta& running_time) {
    EXPECT_TRUE(app_usage.has_app_instance_id());
    EXPECT_THAT(app_usage.app_id(), StrEq(kWebAppUrl));
    EXPECT_THAT(app_usage.app_type(),
                Eq(::apps::ApplicationType::APPLICATION_TYPE_WEB));

    // There is some minor usage (usually in milliseconds) as we attempt to
    // close the browser and before it is actually closed, so we account for
    // that below as we validate reported usage.
    const auto& max_expected_usage = running_time + kWebAppUsageBufferPeriod;
    EXPECT_THAT(app_usage.running_time_ms(),
                AllOf(Ge(running_time.InMilliseconds()),
                      Le(max_expected_usage.InMilliseconds())));

    // Also verify app usage data is reset if not yet cleared from the pref
    // store because this data was reported.
    const auto& app_usage_dict =
        profile()->GetPrefs()->GetDict(::apps::kAppUsageTime);
    if (app_usage_dict.contains(app_usage.app_instance_id())) {
      EXPECT_THAT(
          *app_usage_dict.FindDictByDottedPath(app_usage.app_instance_id())
               ->FindString(::apps::kReportingUsageTimeDurationKey),
          StrEq("0"));
    }
  }

  void VerifyWebAppUsageUKM(std::string_view instance_id,
                            const base::TimeDelta& running_time) {
    const auto entries =
        test_ukm_recorder_->GetEntriesByName(kAppUsageUKMEntryName);
    int usage_time = 0;
    for (const ukm::mojom::UkmEntry* entry : entries) {
      const ::ukm::UkmSource* source =
          test_ukm_recorder_->GetSourceForSourceId(entry->source_id);
      if (!source || source->url() != GURL(kWebAppUrl)) {
        continue;
      }
      usage_time += *(test_ukm_recorder_->GetEntryMetric(entry, "Duration"));
      test_ukm_recorder_->ExpectEntryMetric(entry, "UserDeviceMatrix", 0);
      test_ukm_recorder_->ExpectEntryMetric(entry, "AppType",
                                            (int)::apps::AppTypeName::kWeb);
    }

    // There is some minor usage (usually in milliseconds) as we attempt to
    // close the browser and before it is actually closed, so we account for
    // that below as we validate app usage reported to UKM.
    const auto& max_expected_usage = running_time + kWebAppUsageBufferPeriod;
    EXPECT_THAT(usage_time, AllOf(Ge(running_time.InMilliseconds()),
                                  Le(max_expected_usage.InMilliseconds())));

    // Also verify app usage data is reset if not yet cleared from the pref
    // store because this data was already reported to UKM.
    const auto& app_usage_dict =
        profile()->GetPrefs()->GetDict(::apps::kAppUsageTime);
    if (app_usage_dict.contains(instance_id)) {
      EXPECT_THAT(*app_usage_dict.FindDictByDottedPath(instance_id)
                       ->FindString(::apps::kUsageTimeDurationKey),
                  StrEq("0"));
    }
  }

  void SetAllowedAppReportingTypes(const std::vector<std::string>& app_types) {
    base::Value::List allowed_app_types;
    for (const auto& app_type : app_types) {
      allowed_app_types.Append(app_type);
    }
    profile()->GetPrefs()->SetList(::ash::reporting::kReportAppUsage,
                                   std::move(allowed_app_types));
  }

  Profile* profile() const {
    return ::ash::ProfileHelper::Get()->GetProfileByAccountId(
        affiliation_mixin_.account_id());
  }

  ::syncer::TestSyncService* sync_service() const {
    return static_cast<::syncer::TestSyncService*>(
        SyncServiceFactory::GetForProfile(profile()));
  }

  ::policy::DevicePolicyCrosTestHelper test_helper_;
  ::policy::AffiliationMixin affiliation_mixin_{&mixin_host_, &test_helper_};
  ::ash::CryptohomeMixin crypto_home_mixin_{&mixin_host_};

  base::CallbackListSubscription create_sync_service_subscription_;
  std::unique_ptr<::ukm::TestAutoSetUkmRecorder> test_ukm_recorder_;
};

IN_PROC_BROWSER_TEST_F(AppUsageTelemetrySamplerBrowserTest,
                       PRE_ReportUsageData) {
  // Simple case that sets up the affiliated user through SetUpOnMainThread
  // PRE-condition.
}

IN_PROC_BROWSER_TEST_F(AppUsageTelemetrySamplerBrowserTest, ReportUsageData) {
  // Install webapp and simulate its usage.
  const auto& app_id = InstallStandaloneWebApp(GURL(kWebAppUrl));
  static constexpr base::TimeDelta kAppUsageDuration = base::Minutes(2);
  ::chromeos::MissiveClientTestObserver missive_observer(
      base::BindRepeating(&IsAppUsageTelemetry));
  SimulateAppUsage(app_id, kAppUsageDuration);

  // Force telemetry collection by advancing the timer and verify data that is
  // being enqueued via ERP.
  test::MockClock::Get().Advance(
      metrics::kDefaultAppUsageTelemetryCollectionRate);
  const auto [priority, record] = missive_observer.GetNextEnqueuedRecord();
  AssertRecordData(priority, record);
  MetricData metric_data;
  ASSERT_TRUE(metric_data.ParseFromString(record.data()));
  EXPECT_TRUE(metric_data.has_timestamp_ms());

  // Data reported only includes usage from the web app. Derivative usage from
  // the native Chrome component application (since these leverage the browser)
  // should be filtered out by the policy setting.
  const auto& app_usage_data =
      metric_data.telemetry_data().app_telemetry().app_usage_data();
  ASSERT_THAT(app_usage_data.app_usage().size(), Eq(1));
  const auto& app_usage = app_usage_data.app_usage(0);
  VerifyAppUsage(app_usage, kAppUsageDuration);

  // Trigger upload to UKM by advancing the timer.
  test::MockClock::Get().Advance(kAppUsageUKMReportingInterval);
  ::content::RunAllTasksUntilIdle();
  VerifyWebAppUsageUKM(app_usage.app_instance_id(), kAppUsageDuration);

  // Advance the timer and verify data is cleared by the next upload cycle.
  test::MockClock::Get().Advance(kAppUsageUKMReportingInterval);
  ::content::RunAllTasksUntilIdle();
  ASSERT_FALSE(profile()
                   ->GetPrefs()
                   ->GetDict(::apps::kAppUsageTime)
                   .contains(app_usage.app_instance_id()));
}

IN_PROC_BROWSER_TEST_F(AppUsageTelemetrySamplerBrowserTest,
                       PRE_ReportUsageDataWhenSyncDisabled) {
  // Simple case that sets up the affiliated user through SetUpOnMainThread
  // PRE-condition.
}

IN_PROC_BROWSER_TEST_F(AppUsageTelemetrySamplerBrowserTest,
                       ReportUsageDataWhenSyncDisabled) {
  sync_service()->SetAllowedByEnterprisePolicy(false);

  // Install web app and simulate its usage.
  const auto& app_id = InstallStandaloneWebApp(GURL(kWebAppUrl));
  static constexpr base::TimeDelta kAppUsageDuration = base::Minutes(2);
  ::chromeos::MissiveClientTestObserver missive_observer(
      base::BindRepeating(&IsAppUsageTelemetry));
  SimulateAppUsage(app_id, kAppUsageDuration);

  // Force telemetry collection by advancing the timer and verify data that is
  // being enqueued via ERP.
  test::MockClock::Get().Advance(
      metrics::kDefaultAppUsageTelemetryCollectionRate);
  const auto [priority, record] = missive_observer.GetNextEnqueuedRecord();
  AssertRecordData(priority, record);
  MetricData metric_data;
  ASSERT_TRUE(metric_data.ParseFromString(record.data()));
  EXPECT_TRUE(metric_data.has_timestamp_ms());

  // Data reported only includes usage from the web app. Derivative usage from
  // the native Chrome component application (since these leverage the browser)
  // should be filtered out by the policy setting.
  const auto& app_usage_data =
      metric_data.telemetry_data().app_telemetry().app_usage_data();
  ASSERT_THAT(app_usage_data.app_usage().size(), Eq(1));
  VerifyAppUsage(app_usage_data.app_usage(0), kAppUsageDuration);

  // Advance timer and verify no data is reported to UKM.
  test::MockClock::Get().Advance(kAppUsageUKMReportingInterval);
  ::content::RunAllTasksUntilIdle();
  ASSERT_THAT(
      test_ukm_recorder_->GetEntriesByName(kAppUsageUKMEntryName).size(),
      Eq(0uL));
}

IN_PROC_BROWSER_TEST_F(AppUsageTelemetrySamplerBrowserTest,
                       PRE_ReportUsageDataWhenPolicyDisabled) {
  // Simple case that sets up the affiliated user through SetUpOnMainThread
  // PRE-condition.
}

IN_PROC_BROWSER_TEST_F(AppUsageTelemetrySamplerBrowserTest,
                       ReportUsageDataWhenPolicyDisabled) {
  // Disable policy.
  SetAllowedAppReportingTypes({});

  // Install web app and simulate its usage.
  const auto& app_id = InstallStandaloneWebApp(GURL(kWebAppUrl));
  static constexpr base::TimeDelta kAppUsageDuration = base::Minutes(2);
  ::chromeos::MissiveClientTestObserver missive_observer(
      base::BindRepeating(&IsAppUsageTelemetry));
  SimulateAppUsage(app_id, kAppUsageDuration);

  // Force telemetry collection by advancing the timer and verify no data is
  // being enqueued.
  test::MockClock::Get().Advance(
      metrics::kDefaultAppUsageTelemetryCollectionRate);
  ::content::RunAllTasksUntilIdle();
  ASSERT_FALSE(missive_observer.HasNewEnqueuedRecord());
}

IN_PROC_BROWSER_TEST_F(AppUsageTelemetrySamplerBrowserTest,
                       PRE_ReportUsageDataOnSessionTermination) {
  // Simple case that sets up the affiliated user through SetUpOnMainThread
  // PRE-condition.
}

IN_PROC_BROWSER_TEST_F(AppUsageTelemetrySamplerBrowserTest,
                       ReportUsageDataOnSessionTermination) {
  // Install web app and simulate its usage.
  const auto& app_id = InstallStandaloneWebApp(GURL(kWebAppUrl));
  static constexpr base::TimeDelta kAppUsageDuration = base::Minutes(2);
  ::chromeos::MissiveClientTestObserver missive_observer(
      base::BindRepeating(&IsAppUsageTelemetry));
  SimulateAppUsage(app_id, kAppUsageDuration);

  // Terminate session and verify data being enqueued.
  ::ash::SessionTerminationManager::Get()->StopSession(
      ::login_manager::SessionStopReason::USER_REQUESTS_SIGNOUT);
  const auto [priority, record] = missive_observer.GetNextEnqueuedRecord();
  AssertRecordData(priority, record);
  MetricData metric_data;
  ASSERT_TRUE(metric_data.ParseFromString(record.data()));
  EXPECT_TRUE(metric_data.has_timestamp_ms());

  // Data reported only includes usage from the web app. Derivative usage from
  // the native Chrome component application (since these leverage the browser)
  // should be filtered out by the policy setting.
  const auto& app_usage_data =
      metric_data.telemetry_data().app_telemetry().app_usage_data();
  ASSERT_THAT(app_usage_data.app_usage().size(), Eq(1));
  VerifyAppUsage(app_usage_data.app_usage(0), kAppUsageDuration);
}

}  // namespace
}  // namespace reporting