chromium/chrome/browser/resources/nearby_share/shared/nearby_metrics_logger.ts

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {assertNotReached} from 'chrome://resources/js/assert.js';
import {Visibility} from 'chrome://resources/mojo/chromeos/ash/services/nearby/public/mojom/nearby_share_settings.mojom-webui.js';

/**
 * This enum is tied directly to a UMA enum defined in
 * //tools/metrics/histograms/enums.xml, and should always reflect it (do not
 * change one without changing the other).
 * These values are persisted to logs. Entries should not be renumbered and
 * numeric values should never be reused.
 */
export enum NearbyShareOnboardingFinalState {
  DEVICE_NAME_PAGE = 0,
  VISIBILITY_PAGE = 1,
  COMPLETE = 2,
  INITIAL_PAGE = 3,
  MAX = 4,
}

/**
 * This enum is tied directly to a UMA enum defined in
 * //tools/metrics/histograms/enums.xml, and should always reflect it (do not
 * change one without changing the other).
 * These values are persisted to logs. Entries should not be renumbered and
 * numeric values should never be reused.
 */
export enum NearbyShareOnboardingEntryPoint {
  SETTINGS = 0,
  TRAY = 1,
  SHARE_SHEET = 2,
  NEARBY_DEVICE_TRYING_TO_SHARE_NOTIFICATION = 3,
  MAX = 4,
}

/**
 * This enum is tied directly to a UMA enum defined in
 * //tools/metrics/histograms/enums.xml, and should always reflect it (do not
 * change one without changing the other).
 * These values are persisted to logs. Entries should not be renumbered and
 * numeric values should never be reused.
 */
enum NearbyShareOnboardingFlowEvent {
  ONBOARDING_SHOWN = 1,
  CONFIRM_ON_INITIAL_PAGE = 12,
  CANCEL_ON_INITIAL_PAGE = 13,
  VISIBILITY_CLICKED_ON_INITIAL_PAGE = 14,
  DEVICE_VISIBILITY_PAGE_SHOWN = 141,
  ALL_CONTACTS_SELECTED_AND_CONFIRMED = 1412,
  SOME_CONTACTS_SELECTED_AND_CONFIRMED = 1413,
  HIDDEN_SELECTED_AND_CONFIRMED = 1414,
  MANAGE_CONTACTS_SELECTED = 1415,
  CANCEL_SELECTED_ON_VISIBILITY_PAGE = 1416,
  YOUR_DEVICES_SELECTED_AND_CONFIRMED = 1417,
}

const NearbyShareOnboardingResultHistogramName =
    'Nearby.Share.Onboarding.Result';
const NearbyShareOnboardingEntryPointHistogramName =
    'Nearby.Share.Onboarding.EntryPoint';
const NearbyShareOnboardingDurationHistogramName =
    'Nearby.Share.Onboarding.Duration';
const NearbyShareOnboardingFlowEventHistogramName =
    'Nearby.Share.Onboarding.FlowEvent';
const NearbyShareOnboardingEntryPointResultPrefix = 'Nearby.Share.Onboarding.';
const NearbyShareOnboardingEntryPointResultSuffix = '.Result';

/**
 * Tracks time that onboarding is started. Gets set to null after onboarding is
 * complete as a way to track if onboarding is in progress.
 */
let onboardingInitiatedTimestamp: number|null;

/**
 * Determines which histogram to log to based on entry point.
 */
function processOnboardingEntryPointResultMetrics(
    nearbyShareOnboardingEntryPoint: NearbyShareOnboardingEntryPoint,
    nearbyShareOnboardingFinalState: NearbyShareOnboardingFinalState): void {
  let entryPointString;
  switch (nearbyShareOnboardingEntryPoint) {
    case NearbyShareOnboardingEntryPoint.SETTINGS:
      entryPointString = 'Settings';
      break;
    case NearbyShareOnboardingEntryPoint.TRAY:
      entryPointString = 'Tray';
      break;
    case NearbyShareOnboardingEntryPoint.SHARE_SHEET:
      entryPointString = 'ShareSheet';
      break;
    case NearbyShareOnboardingEntryPoint
        .NEARBY_DEVICE_TRYING_TO_SHARE_NOTIFICATION:
      entryPointString = 'NearbyDeviceTryingToShareNotification';
      break;
    default:
      assertNotReached('Invalid nearbyShareOnboardingEntryPoint');
  }

  chrome.send('metricsHandler:recordInHistogram', [
    NearbyShareOnboardingEntryPointResultPrefix + entryPointString +
        NearbyShareOnboardingEntryPointResultSuffix,
    nearbyShareOnboardingFinalState,
    NearbyShareOnboardingFinalState.MAX,
  ]);
}

export function getOnboardingEntryPoint(url: URL):
    NearbyShareOnboardingEntryPoint {
  let nearbyShareOnboardingEntryPoint: NearbyShareOnboardingEntryPoint =
      NearbyShareOnboardingEntryPoint.MAX;

  if (url.hostname === 'nearby') {
    nearbyShareOnboardingEntryPoint =
        NearbyShareOnboardingEntryPoint.SHARE_SHEET;
  } else if (url.hostname === 'os-settings') {
    const urlParams = new URLSearchParams(url.search);

    nearbyShareOnboardingEntryPoint =
        getOnboardingEntrypointFromQueryParam(urlParams.get('entrypoint'));
  } else {
    assertNotReached('Invalid nearbyShareOnboardingEntryPoint');
  }

  return nearbyShareOnboardingEntryPoint;
}

/**
 * Records the onboarding flow entrypoint and stores the time at which
 * onboarding was initiated. The url param is used to infer the entrypoint.
 */
export function processOnboardingInitiatedMetrics(
    nearbyShareOnboardingEntryPoint: NearbyShareOnboardingEntryPoint): void {
  chrome.send('metricsHandler:recordInHistogram', [
    NearbyShareOnboardingEntryPointHistogramName,
    nearbyShareOnboardingEntryPoint,
    NearbyShareOnboardingEntryPoint.MAX,
  ]);
  // Set time at which onboarding was initiated to track duration.
  onboardingInitiatedTimestamp = window.performance.now();
}

/**
 * Records the one-page onboarding flow entrypoint and stores the time at which
 * one-page onboarding was initiated. The url param is used to infer the
 * entrypoint.
 */
export function processOnePageOnboardingInitiatedMetrics(
    nearbyShareOnboardingEntryPoint: NearbyShareOnboardingEntryPoint): void {
  chrome.send('metricsHandler:recordInHistogram', [
    NearbyShareOnboardingEntryPointHistogramName,
    nearbyShareOnboardingEntryPoint,
    NearbyShareOnboardingEntryPoint.MAX,
  ]);

  chrome.send('metricsHandler:recordSparseHistogram', [
    NearbyShareOnboardingFlowEventHistogramName,
    NearbyShareOnboardingFlowEvent.ONBOARDING_SHOWN,
  ]);

  // Set time at which onboarding was initiated to track duration.
  onboardingInitiatedTimestamp = window.performance.now();
}

function getOnboardingEntrypointFromQueryParam(queryParam: string|null):
    NearbyShareOnboardingEntryPoint {
  switch (queryParam) {
    case 'settings':
      return NearbyShareOnboardingEntryPoint.SETTINGS;
    case 'notification':
      return NearbyShareOnboardingEntryPoint
          .NEARBY_DEVICE_TRYING_TO_SHARE_NOTIFICATION;
    default:
      return NearbyShareOnboardingEntryPoint.TRAY;
  }
}

/**
 * If onboarding was cancelled this function is invoked to record during which
 * step the cancellation occurred.
 */
export function processOnboardingCancelledMetrics(
    nearbyShareOnboardingEntryPointState: NearbyShareOnboardingEntryPoint,
    nearbyShareOnboardingFinalState: NearbyShareOnboardingFinalState): void {
  if (!onboardingInitiatedTimestamp) {
    return;
  }
  chrome.send('metricsHandler:recordInHistogram', [
    NearbyShareOnboardingResultHistogramName,
    nearbyShareOnboardingFinalState,
    NearbyShareOnboardingFinalState.MAX,
  ]);

  processOnboardingEntryPointResultMetrics(
      nearbyShareOnboardingEntryPointState, nearbyShareOnboardingFinalState);
  onboardingInitiatedTimestamp = null;
}

/**
 * If one-page onboarding was cancelled this function is invoked to record
 * during which step the cancellation occurred.
 */
export function processOnePageOnboardingCancelledMetrics(
    nearbyShareOnboardingEntryPointState: NearbyShareOnboardingEntryPoint,
    nearbyShareOnboardingFinalState: NearbyShareOnboardingFinalState): void {
  if (!onboardingInitiatedTimestamp) {
    return;
  }
  chrome.send('metricsHandler:recordInHistogram', [
    NearbyShareOnboardingResultHistogramName,
    nearbyShareOnboardingFinalState,
    NearbyShareOnboardingFinalState.MAX,
  ]);

  chrome.send('metricsHandler:recordSparseHistogram', [
    NearbyShareOnboardingFlowEventHistogramName,
    getOnboardingCancelledFlowEvent(nearbyShareOnboardingFinalState),
  ]);

  processOnboardingEntryPointResultMetrics(
      nearbyShareOnboardingEntryPointState, nearbyShareOnboardingFinalState);
  onboardingInitiatedTimestamp = null;
}

function getOnboardingCancelledFlowEvent(
    nearbyShareOnboardingFinalState: NearbyShareOnboardingFinalState):
    NearbyShareOnboardingFlowEvent {
  switch (nearbyShareOnboardingFinalState) {
    case NearbyShareOnboardingFinalState.INITIAL_PAGE:
      return NearbyShareOnboardingFlowEvent.CANCEL_ON_INITIAL_PAGE;
    case NearbyShareOnboardingFinalState.VISIBILITY_PAGE:
      return NearbyShareOnboardingFlowEvent.CANCEL_SELECTED_ON_VISIBILITY_PAGE;
    default:
      assertNotReached('Invalid final state for cancel event');
  }
}

/**
 * Records a metric for successful onboarding flow completion and the time it
 * took to complete.
 */
export function processOnboardingCompleteMetrics(
    nearbyShareOnboardingEntryPointState: NearbyShareOnboardingEntryPoint):
    void {
  if (!onboardingInitiatedTimestamp) {
    return;
  }
  chrome.send('metricsHandler:recordInHistogram', [
    NearbyShareOnboardingResultHistogramName,
    NearbyShareOnboardingFinalState.COMPLETE,
    NearbyShareOnboardingFinalState.MAX,
  ]);

  chrome.send('metricsHandler:recordMediumTime', [
    NearbyShareOnboardingDurationHistogramName,
    window.performance.now() - onboardingInitiatedTimestamp,
  ]);

  processOnboardingEntryPointResultMetrics(
      nearbyShareOnboardingEntryPointState,
      NearbyShareOnboardingFinalState.COMPLETE);
  onboardingInitiatedTimestamp = null;
}

/**
 * Records a metric for successful one-page onboarding flow completion and the
 * time it took to complete.
 */
export function processOnePageOnboardingCompleteMetrics(
    nearbyShareOnboardingEntryPointState: NearbyShareOnboardingEntryPoint,
    nearbyShareOnboardingFinalState: NearbyShareOnboardingFinalState,
    visibility: Visibility|null): void {
  if (!onboardingInitiatedTimestamp) {
    return;
  }

  chrome.send('metricsHandler:recordSparseHistogram', [
    NearbyShareOnboardingFlowEventHistogramName,
    getOnboardingCompleteFlowEvent(nearbyShareOnboardingFinalState, visibility),
  ]);

  chrome.send('metricsHandler:recordInHistogram', [
    NearbyShareOnboardingResultHistogramName,
    NearbyShareOnboardingFinalState.COMPLETE,
    NearbyShareOnboardingFinalState.MAX,
  ]);

  chrome.send('metricsHandler:recordMediumTime', [
    NearbyShareOnboardingDurationHistogramName,
    window.performance.now() - onboardingInitiatedTimestamp,
  ]);

  processOnboardingEntryPointResultMetrics(
      nearbyShareOnboardingEntryPointState,
      NearbyShareOnboardingFinalState.COMPLETE);
  onboardingInitiatedTimestamp = null;
}

/**
 * Gets the corresponding onboarding complete flow event based on final state
 * and visibility selected.
 */
function getOnboardingCompleteFlowEvent(
    nearbyShareOnboardingFinalState: NearbyShareOnboardingFinalState,
    visibility: Visibility|null): NearbyShareOnboardingFlowEvent {
  switch (nearbyShareOnboardingFinalState) {
    case NearbyShareOnboardingFinalState.INITIAL_PAGE:
      return NearbyShareOnboardingFlowEvent.CONFIRM_ON_INITIAL_PAGE;
    case NearbyShareOnboardingFinalState.VISIBILITY_PAGE:
      return getOnboardingCompleteFlowEventOnVisibilityPage(visibility);
    default:
      assertNotReached('Invalid final state');
  }
}

/**
 * Gets the corresponding onboarding complete flow event on visibility
 * selection page based on final visibility selected.
 */
function getOnboardingCompleteFlowEventOnVisibilityPage(
    visibility: Visibility|null): NearbyShareOnboardingFlowEvent {
  switch (visibility) {
    case Visibility.kAllContacts:
      return NearbyShareOnboardingFlowEvent.ALL_CONTACTS_SELECTED_AND_CONFIRMED;
    case Visibility.kSelectedContacts:
      return NearbyShareOnboardingFlowEvent
          .SOME_CONTACTS_SELECTED_AND_CONFIRMED;
    case Visibility.kYourDevices:
      return NearbyShareOnboardingFlowEvent.YOUR_DEVICES_SELECTED_AND_CONFIRMED;
    case Visibility.kNoOne:
      return NearbyShareOnboardingFlowEvent.HIDDEN_SELECTED_AND_CONFIRMED;
    default:
      assertNotReached('Invalid visibility');
  }
}

/**
 * Records a metric for users clicking the visibility selection button on
 * the initial onboarding page.
 */
export function
processOnePageOnboardingVisibilityButtonOnInitialPageClickedMetrics(): void {
  chrome.send('metricsHandler:recordSparseHistogram', [
    NearbyShareOnboardingFlowEventHistogramName,
    NearbyShareOnboardingFlowEvent.VISIBILITY_CLICKED_ON_INITIAL_PAGE,
  ]);
}

/**
 * Records a metrics for successfully displaying visibility selection page.
 */
export function processOnePageOnboardingVisibilityPageShownMetrics(): void {
  chrome.send('metricsHandler:recordSparseHistogram', [
    NearbyShareOnboardingFlowEventHistogramName,
    NearbyShareOnboardingFlowEvent.DEVICE_VISIBILITY_PAGE_SHOWN,
  ]);
}

/**
 * Records a metrics for users clicking Manage Contacts button on the
 * visibility selection page.
 */
export function processOnePageOnboardingManageContactsMetrics(): void {
  chrome.send('metricsHandler:recordSparseHistogram', [
    NearbyShareOnboardingFlowEventHistogramName,
    NearbyShareOnboardingFlowEvent.MANAGE_CONTACTS_SELECTED,
  ]);
}