blob: dfc7708947b417ec174561de14e7c9913d5a0cca [file] [log] [blame]
// Copyright (c) 2018 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 "chrome/browser/ui/app_list/app_launch_event_logger.h"
#include <cmath>
#include <utility>
#include "base/bind.h"
#include "base/feature_list.h"
#include "base/macros.h"
#include "base/metrics/histogram_macros.h"
#include "base/rand_util.h"
#include "base/stl_util.h"
#include "base/strings/string_util.h"
#include "base/task/post_task.h"
#include "base/time/time.h"
#include "chrome/browser/chromeos/power/ml/recent_events_counter.h"
#include "chrome/browser/chromeos/power/ml/user_activity_ukm_logger_helpers.h"
#include "chrome/browser/metrics/chrome_metrics_service_client.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/common/extensions/manifest_handlers/app_launch_info.h"
#include "components/arc/arc_prefs.h"
#include "components/prefs/pref_service.h"
#include "components/ukm/app_source_url_recorder.h"
#include "extensions/common/extension.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "url/gurl.h"
namespace app_list {
const base::Feature kUkmAppLaunchEventLogging{"UkmAppLaunchEventLogging",
base::FEATURE_ENABLED_BY_DEFAULT};
// Keys for Arc app specific preferences. Defined in
// chrome/browser/ui/app_list/arc/arc_app_list_prefs.cc.
const char AppLaunchEventLogger::kPackageName[] = "package_name";
const char AppLaunchEventLogger::kShouldSync[] = "should_sync";
namespace {
constexpr unsigned int kNumRandomAppsToLog = 25;
const char kArcScheme[] = "arc://";
const char kExtensionSchemeWithDelimiter[] = "chrome-extension://";
constexpr base::TimeDelta kHourDuration = base::TimeDelta::FromHours(1);
constexpr base::TimeDelta kDayDuration = base::TimeDelta::FromDays(1);
constexpr int kMinutesInAnHour = 60;
constexpr int kQuarterHoursInADay = 24 * 4;
constexpr float kTotalHoursBucketSizeMultiplier = 1.25;
constexpr std::array<chromeos::power::ml::Bucket, 2> kClickBuckets = {
{{20, 1}, {200, 10}}};
constexpr std::array<chromeos::power::ml::Bucket, 6>
kTimeSinceLastClickBuckets = {{{60, 1},
{600, 60},
{1200, 300},
{3600, 600},
{18000, 1800},
{86400, 3600}}};
// Returns the nearest bucket for |value|, where bucket sizes are determined
// exponentially, with each bucket size increasing by a factor of |base|.
// The return value is rounded to the nearest integer.
int ExponentialBucket(int value, float base) {
if (base <= 0) {
LOG(DFATAL) << "Base of exponential must be positive.";
return 0;
}
if (value <= 0) {
return 0;
}
return round(pow(base, round(log(value) / log(base))));
}
// Selects a random sample of size |sample_size| from |population|.
std::vector<std::string> Sample(const std::vector<std::string>& population,
unsigned int sample_size) {
std::vector<std::string> sample;
unsigned int index = 0;
// Reservoir sampling.
for (const std::string& candidate : population) {
if (index < sample_size) {
sample.push_back(candidate);
} else {
const uint64_t r = base::RandGenerator(index + 1);
if (r < sample_size) {
sample[r] = candidate;
}
}
index++;
}
return sample;
}
int HourOfDay(base::Time time) {
base::Time::Exploded exploded;
time.LocalExplode(&exploded);
return exploded.hour;
}
int DayOfWeek(base::Time time) {
base::Time::Exploded exploded;
time.LocalExplode(&exploded);
return exploded.day_of_week;
}
} // namespace
AppLaunchEventLogger::AppLaunchEventLogger()
: start_time_(base::Time::Now()),
all_clicks_last_hour_(
std::make_unique<chromeos::power::ml::RecentEventsCounter>(
kHourDuration,
kMinutesInAnHour)),
all_clicks_last_24_hours_(
std::make_unique<chromeos::power::ml::RecentEventsCounter>(
kDayDuration,
kQuarterHoursInADay)),
weak_factory_(this) {
task_runner_ = base::CreateSequencedTaskRunnerWithTraits(
{base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN});
}
AppLaunchEventLogger::~AppLaunchEventLogger() {}
void AppLaunchEventLogger::OnSuggestionChipOrSearchBoxClicked(
const std::string& id,
int suggestion_index,
int launched_from) {
if (!base::FeatureList::IsEnabled(kUkmAppLaunchEventLogging)) {
return;
}
AppLaunchEvent event;
AppLaunchEvent_LaunchedFrom from(
static_cast<AppLaunchEvent_LaunchedFrom>(launched_from));
if (from == AppLaunchEvent_LaunchedFrom_SUGGESTION_CHIP ||
from == AppLaunchEvent_LaunchedFrom_SEARCH_BOX) {
event.set_launched_from(from);
} else {
return;
}
event.set_app_id(RemoveScheme(id));
event.set_index(suggestion_index);
EnforceLoggingPolicy();
task_runner_->PostTask(FROM_HERE,
base::BindOnce(&AppLaunchEventLogger::Log,
weak_factory_.GetWeakPtr(), event));
}
void AppLaunchEventLogger::OnGridClicked(const std::string& id) {
if (!base::FeatureList::IsEnabled(kUkmAppLaunchEventLogging)) {
return;
}
AppLaunchEvent event;
event.set_launched_from(AppLaunchEvent_LaunchedFrom_GRID);
event.set_app_id(id);
EnforceLoggingPolicy();
task_runner_->PostTask(FROM_HERE,
base::BindOnce(&AppLaunchEventLogger::Log,
weak_factory_.GetWeakPtr(), event));
}
void AppLaunchEventLogger::SetAppDataForTesting(
extensions::ExtensionRegistry* registry,
base::DictionaryValue* arc_apps,
base::DictionaryValue* arc_packages) {
testing_ = true;
registry_ = registry;
arc_apps_ = arc_apps;
arc_packages_ = arc_packages;
}
std::string AppLaunchEventLogger::RemoveScheme(const std::string& id) {
std::string app_id(id);
if (!app_id.compare(0, strlen(kExtensionSchemeWithDelimiter),
kExtensionSchemeWithDelimiter)) {
app_id.erase(0, strlen(kExtensionSchemeWithDelimiter));
}
if (!app_id.compare(0, strlen(kArcScheme), kArcScheme)) {
app_id.erase(0, strlen(kArcScheme));
}
if (app_id.size() && !app_id.compare(app_id.size() - 1, 1, "/")) {
app_id.pop_back();
}
return app_id;
}
const GURL& AppLaunchEventLogger::GetLaunchWebURL(
const extensions::Extension* extension) {
return extensions::AppLaunchInfo::GetLaunchWebURL(extension);
}
void AppLaunchEventLogger::OkApp(AppLaunchEvent_AppType app_type,
const std::string& app_id,
const std::string& arc_package_name,
const std::string& pwa_url) {
if (app_features_map_.find(app_id) == app_features_map_.end()) {
AppLaunchFeatures app_launch_features;
app_launch_features.set_app_id(app_id);
app_launch_features.set_app_type(app_type);
if (app_type == AppLaunchEvent_AppType_PWA) {
app_launch_features.set_pwa_url(pwa_url);
} else if (app_type == AppLaunchEvent_AppType_PLAY) {
app_launch_features.set_arc_package_name(arc_package_name);
}
app_features_map_[app_id] = app_launch_features;
}
app_features_map_[app_id].set_is_policy_compliant(true);
}
void AppLaunchEventLogger::EnforceLoggingPolicy() {
// Tests provide installed app information, so don't overwrite that.
if (!testing_) {
Profile* profile = ProfileManager::GetLastUsedProfile();
if (!profile) {
LOG(DFATAL) << "No profile";
return;
}
registry_ = extensions::ExtensionRegistry::Get(profile);
PrefService* pref_service = profile->GetPrefs();
if (pref_service) {
arc_apps_ = pref_service->GetDictionary(arc::prefs::kArcApps);
arc_packages_ = pref_service->GetDictionary(arc::prefs::kArcPackages);
}
}
for (auto& app : app_features_map_) {
app.second.set_is_policy_compliant(false);
}
// Store all Chrome, PWA and bookmark apps.
// registry_ can be nullptr in tests.
if (registry_) {
std::unique_ptr<extensions::ExtensionSet> extensions =
registry_->GenerateInstalledExtensionsSet();
for (const auto& extension : *extensions) {
// Only allow Chrome apps that are from the webstore.
if (extension->from_webstore()) {
OkApp(AppLaunchEvent_AppType_CHROME, extension->id(),
base::EmptyString(), base::EmptyString());
// PWA apps have from_bookmark() true. This will also categorize
// bookmark apps as AppLaunchEvent_AppType_PWA.
} else if (extension->from_bookmark()) {
OkApp(AppLaunchEvent_AppType_PWA, extension->id(), base::EmptyString(),
GetLaunchWebURL(extension.get()).spec());
}
}
}
// Store all Arc apps.
// arc_apps_ and arc_packages_ can be nullptr in tests.
if (arc_apps_ && arc_packages_) {
for (const auto& app : arc_apps_->DictItems()) {
const base::Value* package_name_value = app.second.FindKey(kPackageName);
if (!package_name_value) {
continue;
}
const base::Value* package =
arc_packages_->FindKey(package_name_value->GetString());
// Only allow Arc apps with sync enabled.
if (!package || !package->FindKey(kShouldSync)->GetBool()) {
continue;
}
OkApp(AppLaunchEvent_AppType_PLAY, app.first,
package_name_value->GetString(), base::EmptyString());
}
}
// Remove any apps that are no longer installed or no longer satisfy logging
// policy.
base::EraseIf(app_features_map_,
[](const std::pair<std::string, AppLaunchFeatures>& pair) {
return !pair.second.is_policy_compliant();
});
}
void AppLaunchEventLogger::ProcessClick(const AppLaunchEvent& event,
const base::Time& now) {
auto search = app_features_map_.find(event.app_id());
if (search == app_features_map_.end()) {
return;
}
for (auto& app : app_features_map_) {
// Advance mru index for apps previously clicked on.
if (app.second.has_most_recently_used_index()) {
app.second.set_most_recently_used_index(
app.second.most_recently_used_index() + 1);
}
}
const base::TimeDelta duration = now - start_time_;
AppLaunchFeatures* app_launch_features = &search->second;
if (!app_launch_features->has_most_recently_used_index()) {
// Handle first click on an id.
app_clicks_last_hour_[event.app_id()] =
std::make_unique<chromeos::power::ml::RecentEventsCounter>(
kHourDuration, kMinutesInAnHour);
app_clicks_last_24_hours_[event.app_id()] =
std::make_unique<chromeos::power::ml::RecentEventsCounter>(
kDayDuration, kQuarterHoursInADay);
for (int hour = 0; hour < 24; hour++) {
app_launch_features->add_clicks_each_hour(0);
}
}
app_launch_features->set_most_recently_used_index(0);
app_launch_features->set_last_launched_from(event.launched_from());
app_launch_features->set_total_clicks(app_launch_features->total_clicks() +
1);
app_launch_features->set_time_of_last_click_sec(
now.ToDeltaSinceWindowsEpoch().InSeconds());
const int hour = HourOfDay(now);
app_launch_features->set_clicks_each_hour(
hour, app_launch_features->clicks_each_hour(hour) + 1);
app_clicks_last_hour_[event.app_id()]->Log(duration);
app_clicks_last_24_hours_[event.app_id()]->Log(duration);
app_launch_features->set_clicks_last_hour(
app_clicks_last_hour_[event.app_id()]->GetTotal(duration));
app_launch_features->set_clicks_last_24_hours(
app_clicks_last_24_hours_[event.app_id()]->GetTotal(duration));
}
ukm::SourceId AppLaunchEventLogger::GetSourceId(
AppLaunchEvent_AppType app_type,
const std::string& app_id,
const std::string& arc_package_name,
const std::string& pwa_url) {
if (app_type == AppLaunchEvent_AppType_CHROME) {
return ukm::AppSourceUrlRecorder::GetSourceIdForChromeApp(app_id);
} else if (app_type == AppLaunchEvent_AppType_PWA) {
return ukm::AppSourceUrlRecorder::GetSourceIdForPWA(GURL(pwa_url));
} else if (app_type == AppLaunchEvent_AppType_PLAY) {
return ukm::AppSourceUrlRecorder::GetSourceIdForArc(arc_package_name);
} else {
// Either app is Crostini; or Chrome but not in app store; or Arc but not
// syncable.
return ukm::kInvalidSourceId;
}
}
std::vector<std::string> AppLaunchEventLogger::ChooseAppsToLog(
const std::string clicked_app_id) {
bool has_clicked_app = false;
std::vector<std::string> apps_without_current;
// Do not include the currently clicked app.
for (auto& app : app_features_map_) {
if (app.first == clicked_app_id) {
has_clicked_app = true;
continue;
}
apps_without_current.push_back(app.first);
}
std::vector<std::string> apps(
Sample(apps_without_current, kNumRandomAppsToLog));
if (has_clicked_app) {
apps.push_back(clicked_app_id);
}
return apps;
}
void AppLaunchEventLogger::RecordAppTypeClicked(
AppLaunchEvent_AppType app_type) {
UMA_HISTOGRAM_ENUMERATION("Apps.AppListAppTypeClicked", app_type,
AppLaunchEvent_AppType_AppType_ARRAYSIZE);
}
void AppLaunchEventLogger::LogClicksEachHour(
const AppLaunchFeatures& app_launch_features,
ukm::builders::AppListAppClickData* const app_click_data) {
int bucketized_clicks_each_hour[24];
for (int hour = 0; hour < 24; hour++) {
bucketized_clicks_each_hour[hour] =
Bucketize(app_launch_features.clicks_each_hour(hour), kClickBuckets);
}
if (bucketized_clicks_each_hour[0] != 0) {
app_click_data->SetClicksEachHour00(bucketized_clicks_each_hour[0]);
}
if (bucketized_clicks_each_hour[1] != 0) {
app_click_data->SetClicksEachHour01(bucketized_clicks_each_hour[1]);
}
if (bucketized_clicks_each_hour[2] != 0) {
app_click_data->SetClicksEachHour02(bucketized_clicks_each_hour[2]);
}
if (bucketized_clicks_each_hour[3] != 0) {
app_click_data->SetClicksEachHour03(bucketized_clicks_each_hour[3]);
}
if (bucketized_clicks_each_hour[4] != 0) {
app_click_data->SetClicksEachHour04(bucketized_clicks_each_hour[4]);
}
if (bucketized_clicks_each_hour[5] != 0) {
app_click_data->SetClicksEachHour05(bucketized_clicks_each_hour[5]);
}
if (bucketized_clicks_each_hour[6] != 0) {
app_click_data->SetClicksEachHour06(bucketized_clicks_each_hour[6]);
}
if (bucketized_clicks_each_hour[7] != 0) {
app_click_data->SetClicksEachHour07(bucketized_clicks_each_hour[7]);
}
if (bucketized_clicks_each_hour[8] != 0) {
app_click_data->SetClicksEachHour08(bucketized_clicks_each_hour[8]);
}
if (bucketized_clicks_each_hour[9] != 0) {
app_click_data->SetClicksEachHour09(bucketized_clicks_each_hour[9]);
}
if (bucketized_clicks_each_hour[10] != 0) {
app_click_data->SetClicksEachHour10(bucketized_clicks_each_hour[10]);
}
if (bucketized_clicks_each_hour[11] != 0) {
app_click_data->SetClicksEachHour11(bucketized_clicks_each_hour[11]);
}
if (bucketized_clicks_each_hour[12] != 0) {
app_click_data->SetClicksEachHour12(bucketized_clicks_each_hour[12]);
}
if (bucketized_clicks_each_hour[13] != 0) {
app_click_data->SetClicksEachHour13(bucketized_clicks_each_hour[13]);
}
if (bucketized_clicks_each_hour[14] != 0) {
app_click_data->SetClicksEachHour14(bucketized_clicks_each_hour[14]);
}
if (bucketized_clicks_each_hour[15] != 0) {
app_click_data->SetClicksEachHour15(bucketized_clicks_each_hour[15]);
}
if (bucketized_clicks_each_hour[16] != 0) {
app_click_data->SetClicksEachHour16(bucketized_clicks_each_hour[16]);
}
if (bucketized_clicks_each_hour[17] != 0) {
app_click_data->SetClicksEachHour17(bucketized_clicks_each_hour[17]);
}
if (bucketized_clicks_each_hour[18] != 0) {
app_click_data->SetClicksEachHour18(bucketized_clicks_each_hour[18]);
}
if (bucketized_clicks_each_hour[19] != 0) {
app_click_data->SetClicksEachHour19(bucketized_clicks_each_hour[19]);
}
if (bucketized_clicks_each_hour[20] != 0) {
app_click_data->SetClicksEachHour20(bucketized_clicks_each_hour[20]);
}
if (bucketized_clicks_each_hour[21] != 0) {
app_click_data->SetClicksEachHour21(bucketized_clicks_each_hour[21]);
}
if (bucketized_clicks_each_hour[22] != 0) {
app_click_data->SetClicksEachHour22(bucketized_clicks_each_hour[22]);
}
if (bucketized_clicks_each_hour[23] != 0) {
app_click_data->SetClicksEachHour23(bucketized_clicks_each_hour[23]);
}
}
void AppLaunchEventLogger::Log(AppLaunchEvent app_launch_event) {
auto app = app_features_map_.find(app_launch_event.app_id());
if (app == app_features_map_.end()) {
RecordAppTypeClicked(AppLaunchEvent_AppType_OTHER);
return;
}
RecordAppTypeClicked(app->second.app_type());
ukm::SourceId launch_source_id =
GetSourceId(app->second.app_type(), app_launch_event.app_id(),
app->second.arc_package_name(), app->second.pwa_url());
if (launch_source_id == ukm::kInvalidSourceId) {
return;
}
ukm::builders::AppListAppLaunch app_launch(launch_source_id);
base::Time now(base::Time::Now());
const base::TimeDelta duration = now - start_time_;
all_clicks_last_hour_->Log(duration);
all_clicks_last_24_hours_->Log(duration);
if (app_launch_event.launched_from() ==
AppLaunchEvent_LaunchedFrom_SUGGESTION_CHIP ||
app_launch_event.launched_from() ==
AppLaunchEvent_LaunchedFrom_SEARCH_BOX) {
app_launch.SetPositionIndex(app_launch_event.index());
}
app_launch.SetAppType(app->second.app_type())
.SetLaunchedFrom(app_launch_event.launched_from())
.SetDayOfWeek(DayOfWeek(now))
.SetHourOfDay(HourOfDay(now))
.SetAllClicksLastHour(
Bucketize(all_clicks_last_hour_->GetTotal(duration), kClickBuckets))
.SetAllClicksLast24Hours(Bucketize(
all_clicks_last_24_hours_->GetTotal(duration), kClickBuckets))
.SetTotalHours(ExponentialBucket(duration.InHours(),
kTotalHoursBucketSizeMultiplier))
.Record(ukm::UkmRecorder::Get());
// Log click data about the app clicked on and up to 25 other apps. This
// represents the state of the data immediately before the click.
const std::vector<std::string> apps_to_log =
ChooseAppsToLog(app_launch_event.app_id());
for (std::string app_id : apps_to_log) {
auto app = app_features_map_.find(app_id);
if (app == app_features_map_.end()) {
continue;
}
ukm::SourceId click_data_source_id =
GetSourceId(app->second.app_type(), app->first,
app->second.arc_package_name(), app->second.pwa_url());
if (click_data_source_id == ukm::kInvalidSourceId) {
continue;
}
ukm::builders::AppListAppClickData app_click_data(click_data_source_id);
if (!app->second.has_most_recently_used_index()) {
// This app has not been clicked on this session, so log fewer metrics.
app_click_data.SetAppType(app->second.app_type())
.SetAppLaunchId(launch_source_id)
.Record(ukm::UkmRecorder::Get());
continue;
}
app->second.set_time_since_last_click_sec(
now.ToDeltaSinceWindowsEpoch().InSeconds() -
app->second.time_of_last_click_sec());
LogClicksEachHour(app->second, &app_click_data);
app_click_data.SetAppType(app->second.app_type())
.SetAppLaunchId(launch_source_id)
.SetMostRecentlyUsedIndex(app->second.most_recently_used_index())
.SetTimeSinceLastClick(
Bucketize(app->second.time_since_last_click_sec(),
kTimeSinceLastClickBuckets))
.SetClicksLastHour(
Bucketize(app->second.clicks_last_hour(), kClickBuckets))
.SetClicksLast24Hours(
Bucketize(app->second.clicks_last_24_hours(), kClickBuckets))
.SetTotalClicks(Bucketize(app->second.total_clicks(), kClickBuckets))
.SetLastLaunchedFrom(app->second.last_launched_from())
.Record(ukm::UkmRecorder::Get());
}
ProcessClick(app_launch_event, now);
}
} // namespace app_list