chromium/chrome/browser/ash/app_list/search/search_metrics_manager.cc

// Copyright 2020 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/ash/app_list/search/search_metrics_manager.h"

#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "chrome/browser/ash/app_list/search/common/string_util.h"
#include "chrome/browser/ash/app_list/search/ranking/constants.h"
#include "chrome/browser/ash/app_list/search/search_features.h"
#include "chrome/browser/ash/app_list/search/search_metrics_util.h"
#include "chrome/browser/profiles/profile.h"
#include "components/drive/drive_pref_names.h"
#include "components/metrics/structured/structured_events.h"
#include "components/metrics/structured/structured_metrics_client.h"
#include "components/prefs/pref_service.h"

namespace app_list {
namespace {

using Result = ash::AppListNotifier::Result;
using Location = ash::AppListNotifier::Location;
using Action = SearchMetricsManager::Action;

std::string GetViewString(Location location, const std::u16string& raw_query) {
  std::u16string query;
  base::TrimWhitespace(raw_query, base::TRIM_ALL, &query);
  switch (location) {
    case Location::kList:
      return query.empty() ? "ListZeroState" : "ListSearch";
    case Location::kAnswerCard:
      return "AnswerCard";
    case Location::kRecentApps:
      return "RecentApps";
    case Location::kContinue:
      return "Continue";
    case Location::kImage:
      return "Image";
    default:
      LogError(Error::kUntrackedLocation);
      return "Untracked";
  }
}

std::set<ash::SearchResultType> TypeSet(const std::vector<Result>& results) {
  std::set<ash::SearchResultType> types;
  for (const auto& result : results) {
    types.insert(result.type);
  }
  return types;
}

std::set<ash::ContinueFileSuggestionType> ContinueFileTypeSet(
    const std::vector<Result>& results) {
  std::set<ash::ContinueFileSuggestionType> types;
  for (const auto& result : results) {
    if (result.continue_file_type) {
      types.insert(*result.continue_file_type);
    }
  }
  return types;
}

// Log an action on a set of search result types.
void LogTypeActions(const std::string& action_name,
                    Location location,
                    const std::u16string& query,
                    const std::set<ash::SearchResultType>& types) {
  const std::string histogram_name = base::StrCat(
      {kHistogramPrefix, GetViewString(location, query), ".", action_name});

  for (auto type : types) {
    if (type == ash::SEARCH_RESULT_TYPE_BOUNDARY) {
      LogError(Error::kUntypedResult);
    } else {
      base::UmaHistogramEnumeration(histogram_name, type,
                                    ash::SEARCH_RESULT_TYPE_BOUNDARY);
    }
  }
}

void LogContinueFileTypeActions(
    const std::string& action_name,
    Location location,
    const std::set<ash::ContinueFileSuggestionType>& continue_file_types) {
  if (location != Location::kContinue) {
    return;
  }

  const std::string histogram_name = base::StrCat(
      {kHistogramPrefix, "Continue.FileSuggestionType.", action_name});
  for (auto type : continue_file_types) {
    base::UmaHistogramEnumeration(histogram_name, type);
  }
}

// Log an action for a search result view as a whole.
void LogViewAction(Location location,
                   const std::u16string& query,
                   Action action) {
  const std::string histogram_name =
      base::StrCat({kHistogramPrefix, GetViewString(location, query)});
  base::UmaHistogramEnumeration(histogram_name, action);
}

void LogIsDriveEnabled(Profile* profile) {
  // Logs false for disabled and true for enabled, corresponding to the
  // BooleanEnabled enum.
  base::UmaHistogramBoolean(
      "Apps.AppList.ContinueIsDriveEnabled",
      !profile->GetPrefs()->GetBoolean(drive::prefs::kDisableDrive));
}

void LogContinueMetrics(const std::vector<Result>& results) {
  int drive_count = 0;
  int local_count = 0;
  int help_app_count = 0;
  for (const auto& result : results) {
    switch (result.type) {
      case ash::SearchResultType::ZERO_STATE_DRIVE:
        ++drive_count;
        break;
      case ash::SearchResultType::ZERO_STATE_FILE:
        ++local_count;
        break;
      case ash::SearchResultType::HELP_APP_UPDATES:
        ++help_app_count;
        break;
      case ash::SearchResultType::DESKS_ADMIN_TEMPLATE:
        break;
      default:
        NOTREACHED_IN_MIGRATION() << static_cast<int>(result.type);
    }
  }

  base::UmaHistogramExactLinear("Apps.AppList.Search.ContinueResultCount.Total",
                                results.size(), 10);
  base::UmaHistogramExactLinear("Apps.AppList.Search.ContinueResultCount.Drive",
                                drive_count, 10);
  base::UmaHistogramExactLinear("Apps.AppList.Search.ContinueResultCount.Local",
                                local_count, 10);
  base::UmaHistogramExactLinear(
      "Apps.AppList.Search.ContinueResultCount.HelpApp", help_app_count, 10);
  base::UmaHistogramBoolean("Apps.AppList.Search.DriveContinueResultsShown",
                            drive_count > 0);
}

}  // namespace

SearchMetricsManager::SearchMetricsManager(Profile* profile,
                                           ash::AppListNotifier* notifier) {
  if (notifier) {
    observation_.Observe(notifier);
  } else {
    LogError(Error::kMissingNotifier);
  }

  if (profile)
    LogIsDriveEnabled(profile);
}

SearchMetricsManager::~SearchMetricsManager() = default;

void SearchMetricsManager::OnImpression(Location location,
                                        const std::vector<Result>& results,
                                        const std::u16string& query) {
  LogTypeActions("Impression", location, query, TypeSet(results));
  LogContinueFileTypeActions("Impression", location,
                             ContinueFileTypeSet(results));
  if (!results.empty())
    LogViewAction(location, query, Action::kImpression);
  if (location == Location::kContinue)
    LogContinueMetrics(results);
}

void SearchMetricsManager::OnAbandon(Location location,
                                     const std::vector<Result>& results,
                                     const std::u16string& query) {
  LogTypeActions("Abandon", location, query, TypeSet(results));
  LogContinueFileTypeActions("Abandon", location, ContinueFileTypeSet(results));
  if (!results.empty())
    LogViewAction(location, query, Action::kAbandon);
}

void SearchMetricsManager::OnLaunch(Location location,
                                    const Result& launched,
                                    const std::vector<Result>& shown,
                                    const std::u16string& query) {
  LogViewAction(location, query, Action::kLaunch);

  // Record an ignore for all result types in this view. If other views are
  // shown, they are handled by OnIgnore.
  std::set<ash::SearchResultType> types;
  for (const auto& result : shown) {
    if (result.type != launched.type) {
      types.insert(result.type);
    }
  }

  std::set<ash::ContinueFileSuggestionType> continue_file_types;
  for (const auto& result : shown) {
    if (result.continue_file_type &&
        result.continue_file_type != launched.continue_file_type) {
      continue_file_types.insert(*result.continue_file_type);
    }
  }

  LogTypeActions("Ignore", location, query, types);
  LogContinueFileTypeActions("Ignore", location, continue_file_types);
  LogTypeActions("Launch", location, query, {launched.type});
  if (launched.continue_file_type) {
    LogContinueFileTypeActions("Launch", location,
                               {*launched.continue_file_type});
  }

  // Record the launch index.
  int launched_index = -1;
  for (size_t i = 0; i < shown.size(); ++i) {
    if (shown[i].id == launched.id) {
      launched_index = base::checked_cast<int>(i);
      break;
    }
  }
  const std::string histogram_name = base::StrCat(
      {kHistogramPrefix, GetViewString(location, query), ".LaunchIndex"});
  base::UmaHistogramExactLinear(histogram_name, launched_index, 50);
}

void SearchMetricsManager::OnIgnore(Location location,
                                    const std::vector<Result>& results,
                                    const std::u16string& query) {
  // We have no two concurrently displayed views showing the same result types,
  // so it's safe to log an ignore for all result types here.
  LogTypeActions("Ignore", location, query, TypeSet(results));
  if (!results.empty())
    LogViewAction(location, query, Action::kIgnore);
}

// Log the length of the last query that led to the clicked result - for zero
// state search results, log 0.
void SearchMetricsManager::OnOpen(ash::AppListSearchResultType result_type,
                                  const std::u16string& query) {
  if (ash::IsZeroStateResultType(result_type)) {
    ash::RecordLauncherClickedSearchQueryLength(0);
  } else {
    ash::RecordLauncherClickedSearchQueryLength(query.length());
  }
}

void SearchMetricsManager::OnTrain(LaunchData& launch_data,
                                   const std::string& query) {
  // Record a structured metrics event.
  const base::Time now = base::Time::Now();
  base::Time::Exploded now_exploded;
  now.LocalExplode(&now_exploded);

  metrics::structured::StructuredMetricsClient::Record(
      std::move(metrics::structured::events::v2::launcher_usage::LauncherUsage()
                    .SetTarget(NormalizeId(launch_data.id))
                    .SetApp(last_launched_app_id_)
                    .SetSearchQuery(query)
                    .SetSearchQueryLength(query.size())
                    .SetProviderType(static_cast<int>(launch_data.result_type))
                    .SetHour(now_exploded.hour)
                    .SetScore(launch_data.score)));

  // Only record the last launched app if the hashed logging feature flag is
  // enabled, because it is only used by hashed logging.
  if (IsAppListSearchResultAnApp(launch_data.result_type)) {
    last_launched_app_id_ = NormalizeId(launch_data.id);
  } else if (launch_data.result_type ==
             ash::AppListSearchResultType::kArcAppShortcut) {
    last_launched_app_id_ = RemoveAppShortcutLabel(NormalizeId(launch_data.id));
  }
}

void SearchMetricsManager::OnSearchResultsUpdated(const Scoring& scoring) {
  double score = scoring.BestMatchScore();
  if (search_features::IsLauncherKeywordExtractionScoringEnabled()) {
    UMA_HISTOGRAM_BOOLEAN(
        "Apps.AppList.Scoring.ScoreAboveBestMatchThresholdWithKeywordRanking",
        score > kBestMatchThresholdWithKeywordRanking);
  } else {
    UMA_HISTOGRAM_BOOLEAN(
        "Apps.AppList.Scoring."
        "ScoreAboveBestMatchThresholdWithoutKeywordRanking",
        score > kBestMatchThreshold);
  }
}

}  // namespace app_list