Commit 3cced99b authored by Paul Dyson's avatar Paul Dyson Committed by Commit Bot

Add more click metrics for app launches.

* Add more metrics to UKM event AppListAppLaunch.
* Add a new UKM metric AppListAppClickData to send the
accumulated click data for an app. On an app launch click,
an AppListAppClickData event is sent for the app
clicked on and for five other apps chosen at random.

Also makes user_activity_ukm_logger_helpers a public_dep so
it can be used here.

Bug: 899123
Change-Id: Ie1d6f17673b9548ef6823d05a362fea8992126b9
Reviewed-on: https://chromium-review.googlesource.com/c/1460750Reviewed-by: default avatarSteven Holte <holte@chromium.org>
Reviewed-by: default avatarRobert Kaplow <rkaplow@chromium.org>
Reviewed-by: default avatarJia Meng <jiameng@chromium.org>
Commit-Queue: Paul Dyson <pdyson@chromium.org>
Cr-Commit-Position: refs/heads/master@{#635827}
parent 1e93e6fd
......@@ -27,6 +27,7 @@ source_set("chromeos") {
"//chrome/app/resources:platform_locale_settings",
"//chrome/app/theme:chrome_unscaled_resources",
"//chrome/app/theme:theme_resources",
"//chrome/browser/chromeos/power/ml:user_activity_ukm_logger_helpers",
"//chromeos/dbus:cicerone_proto",
"//chromeos/dbus:concierge_proto",
"//chromeos/dbus:power_manager_proto",
......@@ -57,7 +58,6 @@ source_set("chromeos") {
"//chrome/app:command_ids",
"//chrome/browser/apps/platform_apps",
"//chrome/browser/apps/platform_apps/api",
"//chrome/browser/chromeos/power/ml:user_activity_ukm_logger_helpers",
"//chrome/browser/chromeos/power/ml/smart_dim",
"//chrome/browser/devtools",
"//chrome/browser/extensions",
......
......@@ -4,14 +4,20 @@
#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/task/task_traits.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"
......@@ -35,9 +41,40 @@ const char AppLaunchEventLogger::kShouldSync[] = "should_sync";
namespace {
constexpr int kNumRandomAppsToLog = 5;
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))));
}
int HourOfDay(base::Time time) {
base::Time::Exploded exploded;
time.LocalExplode(&exploded);
......@@ -52,7 +89,17 @@ int DayOfWeek(base::Time time) {
} // namespace
AppLaunchEventLogger::AppLaunchEventLogger() : weak_factory_(this) {
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});
......@@ -70,7 +117,8 @@ void AppLaunchEventLogger::OnSuggestionChipClicked(const std::string& id,
event.set_launched_from(AppLaunchEvent_LaunchedFrom_SUGGESTED);
event.set_app_id(RemoveScheme(id));
event.set_index(suggestion_index);
SetAppInfo(&event);
EnforceLoggingPolicy();
task_runner_->PostTask(FROM_HERE,
base::BindOnce(&AppLaunchEventLogger::Log,
weak_factory_.GetWeakPtr(), event));
......@@ -83,7 +131,8 @@ void AppLaunchEventLogger::OnGridClicked(const std::string& id) {
AppLaunchEvent event;
event.set_launched_from(AppLaunchEvent_LaunchedFrom_GRID);
event.set_app_id(id);
SetAppInfo(&event);
EnforceLoggingPolicy();
task_runner_->PostTask(FROM_HERE,
base::BindOnce(&AppLaunchEventLogger::Log,
weak_factory_.GetWeakPtr(), event));
......@@ -99,7 +148,7 @@ void AppLaunchEventLogger::SetAppDataForTesting(
arc_packages_ = arc_packages;
}
std::string AppLaunchEventLogger::RemoveScheme(const std ::string& id) {
std::string AppLaunchEventLogger::RemoveScheme(const std::string& id) {
std::string app_id(id);
if (!app_id.compare(0, strlen(kExtensionSchemeWithDelimiter),
kExtensionSchemeWithDelimiter)) {
......@@ -130,32 +179,27 @@ const std::string& AppLaunchEventLogger::GetPwaUrl(const std::string& id) {
return base::EmptyString();
}
void AppLaunchEventLogger::SetAppInfo(AppLaunchEvent* event) {
LoadInstalledAppInformation();
if (IsChromeAppFromWebstore(event->app_id())) {
event->set_app_type(AppLaunchEvent_AppType_CHROME);
return;
}
const std::string& pwa_url = GetPwaUrl(event->app_id());
if (pwa_url != base::EmptyString()) {
event->set_app_type(AppLaunchEvent_AppType_PWA);
event->set_pwa_url(pwa_url);
return;
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);
}
const std::string& arc_package_name(GetSyncedArcPackage(event->app_id()));
if (arc_package_name != base::EmptyString()) {
event->set_app_type(AppLaunchEvent_AppType_PLAY);
event->set_arc_package_name(arc_package_name);
return;
app_features_map_[app_id] = app_launch_features;
}
event->set_app_type(AppLaunchEvent_AppType_OTHER);
app_features_map_[app_id].set_is_policy_compliant(true);
}
void AppLaunchEventLogger::LoadInstalledAppInformation() {
void AppLaunchEventLogger::EnforceLoggingPolicy() {
// Tests provide installed app information, so don't overwrite that.
if (testing_)
return;
if (!testing_) {
Profile* profile = ProfileManager::GetLastUsedProfile();
if (!profile) {
LOG(DFATAL) << "No profile";
......@@ -168,81 +212,319 @@ void AppLaunchEventLogger::LoadInstalledAppInformation() {
arc_apps_ = pref_service->GetDictionary(arc::prefs::kArcApps);
arc_packages_ = pref_service->GetDictionary(arc::prefs::kArcPackages);
}
}
}
std::string AppLaunchEventLogger::GetSyncedArcPackage(const std::string& id) {
if (!arc_apps_ || !arc_packages_)
return base::EmptyString();
for (auto& app : app_features_map_) {
app.second.set_is_policy_compliant(false);
}
const base::Value* app = arc_apps_->FindKey(id);
if (!app) {
// App with |id| is not in the list of installed Arc apps.
return base::EmptyString();
// Store all Chrome and PWA 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());
} else {
// Only allow PWA apps on the whitelist.
auto search = pwa_id_url_map_.find(extension->id());
if (search != pwa_id_url_map_.end()) {
OkApp(AppLaunchEvent_AppType_PWA, extension->id(),
base::EmptyString(), search->second);
}
}
}
const base::Value* package_name_value = app->FindKey(kPackageName);
}
// 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) {
return base::EmptyString();
continue;
}
const base::Value* package =
arc_packages_->FindKey(package_name_value->GetString());
if (!package) {
return base::EmptyString();
// 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());
}
if (!package->FindKey(kShouldSync)->GetBool()) {
return base::EmptyString();
}
return package_name_value->GetString();
// 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();
});
}
bool AppLaunchEventLogger::IsChromeAppFromWebstore(const std::string& id) {
if (!registry_)
return false;
const extensions::Extension* extension =
registry_->GetExtensionById(id, extensions::ExtensionRegistry::ENABLED);
if (!extension) {
// App with |id| is not in the list of Chrome extensions.
return false;
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);
}
}
return extension->from_webstore();
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));
}
void AppLaunchEventLogger::Log(AppLaunchEvent app_launch_event) {
UMA_HISTOGRAM_ENUMERATION("Apps.AppListAppTypeClicked",
app_launch_event.app_type(),
AppLaunchEvent_AppType_AppType_ARRAYSIZE);
ukm::SourceId source_id = 0;
// SetAppInfo performed the checks to enforce logging policy. Apps that are
// not to be logged were assigned an |app_type| of OTHER and so are not logged
// here.
if (app_launch_event.app_type() == AppLaunchEvent_AppType_CHROME) {
source_id = ukm::AppSourceUrlRecorder::GetSourceIdForChromeApp(
app_launch_event.app_id());
} else if (app_launch_event.app_type() == AppLaunchEvent_AppType_PWA) {
source_id = ukm::AppSourceUrlRecorder::GetSourceIdForPWA(
GURL(app_launch_event.pwa_url()));
} else if (app_launch_event.app_type() == AppLaunchEvent_AppType_PLAY) {
source_id = ukm::AppSourceUrlRecorder::GetSourceIdForArc(
app_launch_event.arc_package_name());
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; or PWA but not in whitelist.
return ukm::kInvalidSourceId;
}
}
std::vector<std::string> AppLaunchEventLogger::ChooseAppsToLog(
const std::string clicked_app_id) {
int index = 0;
bool has_clicked_app = false;
std::vector<std::string> apps;
// Reservoir sampling, but must also include clicked_app_id if it is
// present.
for (auto& app : app_features_map_) {
if (app.first == clicked_app_id) {
has_clicked_app = true;
continue;
}
if (index < kNumRandomAppsToLog) {
apps.push_back(app.first);
} else {
const uint64_t r = base::RandGenerator(index + 1);
if (r < kNumRandomAppsToLog) {
apps[r] = app.first;
}
}
index++;
}
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);
ukm::builders::AppListAppLaunch app_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_SUGGESTED) {
app_launch.SetPositionIndex(app_launch_event.index());
}
app_launch.SetAppType(app_launch_event.app_type())
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 five other apps chosen at
// random. 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
......@@ -5,6 +5,8 @@
#ifndef CHROME_BROWSER_UI_APP_LIST_APP_LAUNCH_EVENT_LOGGER_H_
#define CHROME_BROWSER_UI_APP_LIST_APP_LAUNCH_EVENT_LOGGER_H_
#include <memory>
#include <string>
#include <vector>
#include "base/containers/flat_map.h"
......@@ -14,6 +16,21 @@
#include "base/values.h"
#include "chrome/browser/ui/app_list/app_launch_event_logger.pb.h"
#include "extensions/browser/extension_registry.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
namespace chromeos {
namespace power {
namespace ml {
class RecentEventsCounter;
} // namespace ml
} // namespace power
} // namespace chromeos
namespace ukm {
namespace builders {
class AppListAppClickData;
} // namespace builders
} // namespace ukm
namespace app_list {
......@@ -24,15 +41,19 @@ namespace app_list {
// apps the keys are based upon the app id, for Arc apps the keys are based upon
// a hash of the package name and for PWAs the keys are the urls associated with
// the PWA.
// At the time of app launch this class logs metrics about the app clicked on
// and another five apps that were not clicked on, chosen at random.
class AppLaunchEventLogger {
public:
AppLaunchEventLogger();
~AppLaunchEventLogger();
// Processes a click on an app in the suggestion chip and logs the resulting
// metrics in UKM.
// metrics in UKM. This method calls EnforceLoggingPolicy() to ensure the
// logging policy is complied with.
void OnSuggestionChipClicked(const std::string& id, int suggestion_index);
// Processes a click on an app located in the grid of apps in the launcher and
// logs the resulting metrics in UKM.
// logs the resulting metrics in UKM. This method calls EnforceLoggingPolicy()
// to ensure the logging policy is complied with.
void OnGridClicked(const std::string& id);
// Provides values to be used when testing.
void SetAppDataForTesting(extensions::ExtensionRegistry* registry,
......@@ -43,7 +64,7 @@ class AppLaunchEventLogger {
static const char kShouldSync[];
private:
// Remove any leading "chrome-extension://" or "arc://". Also remove any
// Removes any leading "chrome-extension://" or "arc://". Also remove any
// trailing "/".
std::string RemoveScheme(const std::string& id);
// Creates the mapping from PWA app id to PWA url. This mapping also acts as a
......@@ -52,18 +73,33 @@ class AppLaunchEventLogger {
// Gets the PWA url from its app id. Returns base::EmptyString() if no match
// found.
const std::string& GetPwaUrl(const std::string& id);
// Sets the |event|'s app type based on the id. Also sets the url for PWA apps
// and package name for Arc apps.
// Logging policy is enforced here by only assigning a meaningful app type to
// apps that are allowed to be logged. An app type of OTHER is assigned to
// apps that cannot be logged by policy.
void SetAppInfo(AppLaunchEvent* event);
// Loads the information used to determine which apps can be logged.
void LoadInstalledAppInformation();
// Gets the Arc package name for synced apps. Returns base::EmptyString() if
// app not being synced or app id not found.
std::string GetSyncedArcPackage(const std::string& id);
bool IsChromeAppFromWebstore(const std::string&);
// Marks app as ok for policy compliance. If the app is not in
// |app_features_map_| then add it.
void OkApp(AppLaunchEvent_AppType app_type,
const std::string& app_id,
const std::string& arc_package_name,
const std::string& pwa_url);
// Enforces logging policy, ensuring that the |app_features_map_| only
// contains apps that are allowed to be logged. All apps are rechecked in case
// they have been uninstalled since the previous check.
void EnforceLoggingPolicy();
// Updates the app data following a click.
void ProcessClick(const AppLaunchEvent& event, const base::Time& now);
// Returns a source id. |arc_package_name| is only required for Arc apps,
// |pwa_url| is only required for PWA apps.
ukm::SourceId GetSourceId(AppLaunchEvent_AppType app_type,
const std::string& app_id,
const std::string& arc_package_name,
const std::string& pwa_url);
// Randomly chooses up to five apps to log, plus the app clicked on.
std::vector<std::string> ChooseAppsToLog(const std::string clicked_app_id);
// Records a UMA histogram of the app type clicked on.
void RecordAppTypeClicked(AppLaunchEvent_AppType app_type);
// Helper function to log the clicks each hour metrics.
void LogClicksEachHour(
const AppLaunchFeatures& app_launch_features,
ukm::builders::AppListAppClickData* const app_click_data);
// Logs the app click using UKM.
void Log(AppLaunchEvent app_launch_event);
......@@ -77,6 +113,31 @@ class AppLaunchEventLogger {
const base::DictionaryValue* arc_packages_;
// The Chrome extension registry.
extensions::ExtensionRegistry* registry_;
// A map from app id to features. Only contains apps satisfying logging
// policy.
base::flat_map<std::string, AppLaunchFeatures> app_features_map_;
// A map from app id to a counter of the number of clicks in the last hour.
// Has a time resolution one minute.
base::flat_map<std::string,
std::unique_ptr<chromeos::power::ml::RecentEventsCounter>>
app_clicks_last_hour_;
// A map from app id to a counter of the number of clicks in the last 24
// hours. Has a time resolution of 15 minutes.
base::flat_map<std::string,
std::unique_ptr<chromeos::power::ml::RecentEventsCounter>>
app_clicks_last_24_hours_;
// The time this class was instantiated. Allows duration to be calculated.
base::Time start_time_;
// A counter for the click in the last hour. Has a time resolution of 1
// minute.
const std::unique_ptr<chromeos::power::ml::RecentEventsCounter>
all_clicks_last_hour_;
// A counter for the clicks in the last 24 hours. Has a time resolution of 15
// minutes.
const std::unique_ptr<chromeos::power::ml::RecentEventsCounter>
all_clicks_last_24_hours_;
// Used to prevent overwriting of parameters that are set for tests.
bool testing_ = false;
......
......@@ -35,3 +35,27 @@ message AppLaunchEvent {
// The URL for PWA apps.
optional string pwa_url = 6;
}
// The App features used for training and inference.
message AppLaunchFeatures {
// The id of the app as a 32 character string. e.g.
// "pjkljhegncpnkpknbcohdijeoejaedia".
optional string app_id = 1;
optional AppLaunchEvent.AppType app_type = 2;
optional string arc_package_name = 3;
optional string pwa_url = 4;
optional int32 sequence_number = 5;
optional int32 most_recently_used_index = 6;
// Not logged in UKM.
optional int64 time_of_last_click_sec = 7;
optional int32 time_since_last_click_sec = 8;
optional int32 clicks_last_hour = 9;
optional int32 clicks_last_24_hours = 10;
optional int32 total_clicks = 11;
repeated int32 clicks_each_hour = 12;
optional AppLaunchEvent.LaunchedFrom last_launched_from = 13;
// Used when checking app for compliance. Should always be true outside of the
// checking procedure, which removes apps with |is_policy_compliant| false.
// Not logged in UKM.
optional bool is_policy_compliant = 14;
}
......@@ -4,9 +4,12 @@
#include "chrome/browser/ui/app_list/app_launch_event_logger.h"
#include <memory>
#include "base/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/scoped_task_environment.h"
#include "chrome/browser/ui/app_list/search/chrome_search_result.h"
#include "components/arc/arc_prefs.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
......@@ -46,10 +49,15 @@ class AppLaunchEventLoggerTest : public testing::Test {
};
TEST_F(AppLaunchEventLoggerTest, CheckUkmCodePWA) {
extensions::ExtensionRegistry registry(nullptr);
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("test").SetID(kPhotosPWAApp).Build();
registry.AddEnabled(extension);
GURL url("https://photos.google.com/");
AppLaunchEventLogger app_launch_event_logger_;
app_launch_event_logger_.SetAppDataForTesting(nullptr, nullptr, nullptr);
app_launch_event_logger_.SetAppDataForTesting(&registry, nullptr, nullptr);
app_launch_event_logger_.OnGridClicked(kPhotosPWAApp);
scoped_task_environment_.RunUntilIdle();
......@@ -58,6 +66,20 @@ TEST_F(AppLaunchEventLoggerTest, CheckUkmCodePWA) {
ASSERT_EQ(1ul, entries.size());
const auto* entry = entries.back();
test_ukm_recorder_.ExpectEntrySourceHasUrl(entry, url);
test_ukm_recorder_.ExpectEntryMetric(entry, "AllClicksLast24Hours", 1);
test_ukm_recorder_.ExpectEntryMetric(entry, "AllClicksLastHour", 1);
test_ukm_recorder_.ExpectEntryMetric(entry, "AppType", 3);
test_ukm_recorder_.ExpectEntryMetric(entry, "LaunchedFrom", 1);
test_ukm_recorder_.ExpectEntryMetric(entry, "TotalHours", 0);
const auto click_entries =
test_ukm_recorder_.GetEntriesByName("AppListAppClickData");
ASSERT_EQ(1ul, click_entries.size());
const auto* photos_entry = click_entries.back();
test_ukm_recorder_.ExpectEntrySourceHasUrl(photos_entry, url);
test_ukm_recorder_.ExpectEntryMetric(photos_entry, "AppLaunchId",
entry->source_id);
test_ukm_recorder_.ExpectEntryMetric(photos_entry, "AppType", 3);
}
TEST_F(AppLaunchEventLoggerTest, CheckUkmCodeChrome) {
......@@ -85,6 +107,8 @@ TEST_F(AppLaunchEventLoggerTest, CheckUkmCodeChrome) {
ASSERT_EQ(1ul, entries.size());
const auto* entry = entries.back();
test_ukm_recorder_.ExpectEntrySourceHasUrl(entry, url);
test_ukm_recorder_.ExpectEntryMetric(entry, "AppType", 1);
test_ukm_recorder_.ExpectEntryMetric(entry, "LaunchedFrom", 1);
}
TEST_F(AppLaunchEventLoggerTest, CheckUkmCodeArc) {
......@@ -113,13 +137,95 @@ TEST_F(AppLaunchEventLoggerTest, CheckUkmCodeArc) {
ASSERT_EQ(1ul, entries.size());
const auto* entry = entries.back();
test_ukm_recorder_.ExpectEntrySourceHasUrl(entry, url);
test_ukm_recorder_.ExpectEntryMetric(entry, "AppType", 2);
test_ukm_recorder_.ExpectEntryMetric(entry, "LaunchedFrom", 1);
}
TEST_F(AppLaunchEventLoggerTest, CheckMultipleClicks) {
// Click on PWA photos, then Chrome Maps, then PWA Photos again.
extensions::ExtensionRegistry registry(nullptr);
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("test").SetID(kPhotosPWAApp).Build();
registry.AddEnabled(extension);
GURL photos_url("https://photos.google.com/");
base::Value package(base::Value::Type::DICTIONARY);
package.SetKey(AppLaunchEventLogger::kShouldSync, base::Value(true));
auto packages = std::make_unique<base::DictionaryValue>();
packages->SetKey(kMapsPackageName, package.Clone());
base::Value app(base::Value::Type::DICTIONARY);
app.SetKey(AppLaunchEventLogger::kPackageName, base::Value(kMapsPackageName));
auto arc_apps = std::make_unique<base::DictionaryValue>();
arc_apps->SetKey(kMapsArcApp, app.Clone());
GURL maps_url("app://play/gbpfhehadcpcndihhameeacbdmbjbhgi");
AppLaunchEventLogger app_launch_event_logger_;
app_launch_event_logger_.SetAppDataForTesting(&registry, arc_apps.get(),
packages.get());
app_launch_event_logger_.OnGridClicked(kPhotosPWAApp);
app_launch_event_logger_.OnGridClicked(kMapsArcApp);
app_launch_event_logger_.OnSuggestionChipClicked(kPhotosPWAApp, 2);
app_launch_event_logger_.OnGridClicked(kPhotosPWAApp);
scoped_task_environment_.RunUntilIdle();
const auto entries = test_ukm_recorder_.GetEntriesByName("AppListAppLaunch");
ASSERT_EQ(4ul, entries.size());
const auto* entry = entries.back();
test_ukm_recorder_.ExpectEntrySourceHasUrl(entry, photos_url);
test_ukm_recorder_.ExpectEntryMetric(entry, "AllClicksLast24Hours", 4);
test_ukm_recorder_.ExpectEntryMetric(entry, "AllClicksLastHour", 4);
test_ukm_recorder_.ExpectEntryMetric(entry, "AppType", 3);
test_ukm_recorder_.ExpectEntryMetric(entry, "LaunchedFrom", 1);
test_ukm_recorder_.ExpectEntryMetric(entry, "TotalHours", 0);
const auto click_entries =
test_ukm_recorder_.GetEntriesByName("AppListAppClickData");
ASSERT_EQ(8ul, click_entries.size());
// Examine the last two events, which are created by the last click.
const auto* maps_entry = click_entries.at(6);
const auto* photos_entry = click_entries.at(7);
if (test_ukm_recorder_.GetSourceForSourceId(maps_entry->source_id)->url() ==
photos_url) {
const auto* tmp_entry = photos_entry;
photos_entry = maps_entry;
maps_entry = tmp_entry;
}
test_ukm_recorder_.ExpectEntrySourceHasUrl(photos_entry, photos_url);
test_ukm_recorder_.ExpectEntrySourceHasUrl(maps_entry, maps_url);
test_ukm_recorder_.ExpectEntryMetric(photos_entry, "AppLaunchId",
entry->source_id);
test_ukm_recorder_.ExpectEntryMetric(maps_entry, "AppLaunchId",
entry->source_id);
test_ukm_recorder_.ExpectEntryMetric(photos_entry, "ClicksLast24Hours", 2);
test_ukm_recorder_.ExpectEntryMetric(maps_entry, "ClicksLast24Hours", 1);
test_ukm_recorder_.ExpectEntryMetric(photos_entry, "ClicksLastHour", 2);
test_ukm_recorder_.ExpectEntryMetric(maps_entry, "ClicksLastHour", 1);
test_ukm_recorder_.ExpectEntryMetric(photos_entry, "MostRecentlyUsedIndex",
0);
test_ukm_recorder_.ExpectEntryMetric(maps_entry, "MostRecentlyUsedIndex", 1);
test_ukm_recorder_.ExpectEntryMetric(photos_entry, "TimeSinceLastClick", 0);
test_ukm_recorder_.ExpectEntryMetric(photos_entry, "TotalClicks", 2);
test_ukm_recorder_.ExpectEntryMetric(maps_entry, "TotalClicks", 1);
test_ukm_recorder_.ExpectEntryMetric(photos_entry, "LastLaunchedFrom", 2);
test_ukm_recorder_.ExpectEntryMetric(maps_entry, "LastLaunchedFrom", 1);
}
TEST_F(AppLaunchEventLoggerTest, CheckUkmCodeSuggestionChip) {
extensions::ExtensionRegistry registry(nullptr);
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("test").SetID(kPhotosPWAApp).Build();
registry.AddEnabled(extension);
GURL url("https://photos.google.com/");
AppLaunchEventLogger app_launch_event_logger_;
app_launch_event_logger_.SetAppDataForTesting(nullptr, nullptr, nullptr);
app_launch_event_logger_.SetAppDataForTesting(&registry, nullptr, nullptr);
app_launch_event_logger_.OnSuggestionChipClicked(kPhotosPWAApp, 2);
scoped_task_environment_.RunUntilIdle();
......
......@@ -165,6 +165,20 @@ be describing additional metrics about the same event.
the app id, for Play apps the keys are based upon a hash of the package name
and for PWAs the keys are the urls associated with the PWA.
</summary>
<metric name="AllClicksLast24Hours">
<summary>
Total number of clicks launching logged apps in the last 24 hours.
Accurate to the nearest 15 minutes. Bucketing: values above 20 rounded
down to the nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="AllClicksLastHour">
<summary>
Total number of clicks launching logged apps in the last hour. Accurate to
the nearest minute. Bucketing: values above 20 rounded down to the nearest
10. Maximum value of 200.
</summary>
</metric>
<metric name="AppType">
<summary>
The type of app. 1: CHROME, 2: PLAY, 3: PWA.
......@@ -210,6 +224,249 @@ be describing additional metrics about the same event.
first position.
</summary>
</metric>
<metric name="TotalHours">
<summary>
Number of hours in the current session up to this event. Bucketing:
exponential buckets, increasing in size by 25%, rounded to the nearest
integer. i.e. 0, 1, 2, 3, 4, 5, 6, 7, 9, 12, 15, 18, 23, 28, 36, 44...
</summary>
</metric>
</event>
<event name="AppListAppClickData">
<owner>pdyson@chromium.org</owner>
<summary>
AppListAppClickData events contain click history metrics for an app. These
events are recorded when an app is launched from the launcher on ChromeOS,
but may be for an app different from the app launched. See event
AppListAppLaunch for more detail on the keys used.
</summary>
<metric name="AppLaunchId">
<summary>
The ID of the app launch event.
</summary>
</metric>
<metric name="AppType">
<summary>
The type of app. 1: CHROME, 2: PLAY, 3: PWA.
</summary>
</metric>
<metric name="ClicksEachHour00">
<summary>
The number of clicks on this app in the current session that occurred
between 12:00am and 1:00am. Bucketing: values above 20 rounded down to the
nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour01">
<summary>
The number of clicks on this app in the current session that occurred
between 1:00am and 2:00am. Bucketing: values above 20 rounded down to the
nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour02">
<summary>
The number of clicks on this app in the current session that occurred
between 2:00am and 3:00am. Bucketing: values above 20 rounded down to the
nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour03">
<summary>
The number of clicks on this app in the current session that occurred
between 3:00am and 4:00am. Bucketing: values above 20 rounded down to the
nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour04">
<summary>
The number of clicks on this app in the current session that occurred
between 4:00am and 5:00am. Bucketing: values above 20 rounded down to the
nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour05">
<summary>
The number of clicks on this app in the current session that occurred
between 5:00am and 6:00am. Bucketing: values above 20 rounded down to the
nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour06">
<summary>
The number of clicks on this app in the current session that occurred
between 6:00am and 7:00am. Bucketing: values above 20 rounded down to the
nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour07">
<summary>
The number of clicks on this app in the current session that occurred
between 7:00am and 8:00am. Bucketing: values above 20 rounded down to the
nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour08">
<summary>
The number of clicks on this app in the current session that occurred
between 8:00am and 9:00am. Bucketing: values above 20 rounded down to the
nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour09">
<summary>
The number of clicks on this app in the current session that occurred
between 9:00am and 10:00am. Bucketing: values above 20 rounded down to the
nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour10">
<summary>
The number of clicks on this app in the current session that occurred
between 10:00am and 11:00am. Bucketing: values above 20 rounded down to
the nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour11">
<summary>
The number of clicks on this app in the current session that occurred
between 11:00am and 12:00pm. Bucketing: values above 20 rounded down to
the nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour12">
<summary>
The number of clicks on this app in the current session that occurred
between 12:00pm and 1:00pm. Bucketing: values above 20 rounded down to the
nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour13">
<summary>
The number of clicks on this app in the current session that occurred
between 1:00pm and 2:00pm. Bucketing: values above 20 rounded down to the
nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour14">
<summary>
The number of clicks on this app in the current session that occurred
between 2:00pm and 3:00pm. Bucketing: values above 20 rounded down to the
nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour15">
<summary>
The number of clicks on this app in the current session that occurred
between 3:00pm and 4:00pm. Bucketing: values above 20 rounded down to the
nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour16">
<summary>
The number of clicks on this app in the current session that occurred
between 4:00pm and 5:00pm. Bucketing: values above 20 rounded down to the
nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour17">
<summary>
The number of clicks on this app in the current session that occurred
between 5:00pm and 6:00pm. Bucketing: values above 20 rounded down to the
nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour18">
<summary>
The number of clicks on this app in the current session that occurred
between 6:00pm and 7:00pm. Bucketing: values above 20 rounded down to the
nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour19">
<summary>
The number of clicks on this app in the current session that occurred
between 7:00pm and 8:00pm. Bucketing: values above 20 rounded down to the
nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour20">
<summary>
The number of clicks on this app in the current session that occurred
between 8:00pm and 9:00pm. Bucketing: values above 20 rounded down to the
nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour21">
<summary>
The number of clicks on this app in the current session that occurred
between 9:00pm and 10:00pm. Bucketing: values above 20 rounded down to the
nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour22">
<summary>
The number of clicks on this app in the current session that occurred
between 10:00pm and 11:00pm. Bucketing: values above 20 rounded down to
the nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksEachHour23">
<summary>
The number of clicks on this app in the current session that occurred
between 11:00pm and 12:00am. Bucketing: values above 20 rounded down to
the nearest 10. Maximum value of 200.
</summary>
</metric>
<metric name="ClicksLast24Hours">
<summary>
The number of clicks on this app in the last 24 hours. Accurate to 15
minutes. Bucketing: values above 20 rounded down to the nearest 10.
Maximum value of 200.
</summary>
</metric>
<metric name="ClicksLastHour">
<summary>
The number of clicks on this app in the last hour. Accurate to one minute.
Bucketing: values above 20 rounded down to the nearest 10. Maximum value
of 200.
</summary>
</metric>
<metric name="LastLaunchedFrom">
<summary>
Where the app was last launched from. 1: GRID, 2: SUGGESTED, 3: SHELF.
</summary>
</metric>
<metric name="MostRecentlyUsedIndex">
<summary>
Index recording when in the sequence of app launch clicks the app was last
clicked on. 0: most recent app clicked on, 1: app clicked immediately
before the most recent, etc.
</summary>
</metric>
<metric name="SequenceNumber">
<summary>
The sequence number of the AppListAppLaunch event that corresponds to this
AppListAppClickData event. There will be six AppListAppClickData events
for each sequence number.
</summary>
</metric>
<metric name="TimeSinceLastClick">
<summary>
Time since this app was last clicked on to launch. In seconds, bucketed:
[1, 59] to the nearest second, [60, 599] to the nearest minute, [600,
1199] to the nearest 5 minutes, [1200, 3599] to the nearest 10 minutes,
[3600, 17999] to the nearest 30 minutes, [18000, 86400] to the nearest
hour.
</summary>
</metric>
<metric name="TotalClicks">
<summary>
Total number of clicks launching this app in this sesion. Bucketing:
values above 20 rounded down to the nearest 10. Maximum value of 200.
</summary>
</metric>
</event>
<event name="Autofill.DeveloperEngagement">
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment