[Bluetooth] Record metrics for user's time taken to select devices.
Records how long it takes for the user to select a device either after
they open the UI (either Settings or System Tray) and Bluetooth is on,
or after Bluetooth turns on while the UI is open.
Note that both UI surfaces' code paths for the metric eventually resolve
to a single helper method in //device/bluetooth/chromeos.
This metric will be used to drive both improving the UX of our UI
surfaces, and possibly increasing discovery performance.
This metric is suffixed by UI surface; then by pairing state; and
finally, if the device was not paired, by transport type. These
allows us to determine if one UI surface is less UX friendly than the
other (i.e., it takes longer to find/select the desired device in one);
ensure that users are quickly finding their already paired devices;
and determine if one particular transport type is slower to discover.
See go/cros-bt-metrics for more.
Bug: 953149
Change-Id: Ie245a003ada08d620ed5825839816c5db27ca491
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1655694
Commit-Queue: Ryan Hansberry <hansberry@chromium.org>
Reviewed-by: Brian White <bcwhite@chromium.org>
Reviewed-by: Steven Bennetts <stevenjb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#669327}
diff --git a/ash/system/bluetooth/tray_bluetooth_helper_legacy.cc b/ash/system/bluetooth/tray_bluetooth_helper_legacy.cc
index 7e769bb..180c157 100644
--- a/ash/system/bluetooth/tray_bluetooth_helper_legacy.cc
+++ b/ash/system/bluetooth/tray_bluetooth_helper_legacy.cc
@@ -18,6 +18,8 @@
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
+#include "base/time/default_clock.h"
+#include "base/time/time.h"
#include "device/bluetooth/bluetooth_adapter.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "device/bluetooth/bluetooth_device.h"
@@ -187,6 +189,8 @@
}
void TrayBluetoothHelperLegacy::StartBluetoothDiscovering() {
+ discovery_start_timestamp_ = base::DefaultClock::GetInstance()->Now();
+
if (HasBluetoothDiscoverySession()) {
LOG(WARNING) << "Already have active Bluetooth device discovery session.";
return;
@@ -200,6 +204,8 @@
}
void TrayBluetoothHelperLegacy::StopBluetoothDiscovering() {
+ discovery_start_timestamp_ = base::Time();
+
should_run_discovery_ = false;
if (!HasBluetoothDiscoverySession()) {
LOG(WARNING) << "No active Bluetooth device discovery session.";
@@ -219,6 +225,14 @@
return;
}
+ if (!discovery_start_timestamp_.is_null()) {
+ device::RecordDeviceSelectionDuration(
+ base::DefaultClock::GetInstance()->Now() - discovery_start_timestamp_,
+ device::BluetoothUiSurface::kSystemTray, device->IsPaired(),
+ device->GetType());
+ discovery_start_timestamp_ = base::Time();
+ }
+
// Extra consideration taken for already paired devices, for metrics
// collection.
if (device->IsPaired()) {
diff --git a/ash/system/bluetooth/tray_bluetooth_helper_legacy.h b/ash/system/bluetooth/tray_bluetooth_helper_legacy.h
index 119009e..a9aa123 100644
--- a/ash/system/bluetooth/tray_bluetooth_helper_legacy.h
+++ b/ash/system/bluetooth/tray_bluetooth_helper_legacy.h
@@ -17,9 +17,13 @@
#include "device/bluetooth/bluetooth_adapter.h"
#include "services/device/public/mojom/bluetooth_system.mojom.h"
+namespace base {
+class Time;
+} // namespace base
+
namespace device {
class BluetoothDiscoverySession;
-}
+} // namespace device
namespace ash {
@@ -71,6 +75,11 @@
device::mojom::BluetoothSystem::State last_state_ =
device::mojom::BluetoothSystem::State::kUnavailable;
+ // The time at which discovery started, effectively when the user opened the
+ // System Tray Bluetooth options with Bluetooth on, or when Bluetooth turned
+ // on while the Bluetooth options were open.
+ base::Time discovery_start_timestamp_;
+
// Object could be deleted during a prolonged Bluetooth operation.
base::WeakPtrFactory<TrayBluetoothHelperLegacy> weak_ptr_factory_;
diff --git a/chrome/browser/resources/settings/bluetooth_page/bluetooth_subpage.js b/chrome/browser/resources/settings/bluetooth_page/bluetooth_subpage.js
index f22bdd00..7e2c35f3 100644
--- a/chrome/browser/resources/settings/bluetooth_page/bluetooth_subpage.js
+++ b/chrome/browser/resources/settings/bluetooth_page/bluetooth_subpage.js
@@ -143,6 +143,18 @@
type: Number,
value: 1000,
},
+
+ /**
+ * The time in milliseconds at which discovery was started attempt (when the
+ * page was opened with Bluetooth on, or when Bluetooth turned on while the
+ * page was active).
+ * @private {?number}
+ */
+ discoveryStartTimestampMs_: {
+ type: Number,
+ value: null,
+ },
+
},
observers: [
@@ -392,6 +404,10 @@
this.openDialog_();
}
+ if (isPaired !== undefined && device.transport !== undefined) {
+ this.recordDeviceSelectionDuration_(isPaired, device.transport);
+ }
+
const address = device.address;
this.bluetoothPrivate.connect(address, result => {
if (isPaired) {
@@ -492,10 +508,12 @@
this.updateTimerId_ =
window.setInterval(this.refreshBluetoothList_.bind(this),
this.listUpdateFrequencyMs);
+ this.discoveryStartTimestampMs_ = Date.now();
return;
}
window.clearInterval(this.updateTimerId_);
this.updateTimerId_ = undefined;
+ this.discoveryStartTimestampMs_ = null;
this.deviceList_ = [];
},
@@ -542,5 +560,31 @@
}
chrome.bluetoothPrivate.recordReconnection(success);
- }
+ },
+
+ /**
+ * Record metrics for how long it took between when discovery started on the
+ * Settings page, and the user selected the device they wanted to connect to.
+ * @param {!boolean} wasPaired If the selected device was already
+ * paired.
+ * @param {!chrome.bluetooth.Transport} transport The transport type
+ * of the device.
+ * @private
+ */
+ recordDeviceSelectionDuration_: function(wasPaired, transport) {
+ if (!this.discoveryStartTimestampMs_) {
+ // It's not necessarily an error that |discoveryStartTimestampMs_| isn't
+ // present; it's intentionally cleared after the first device selection
+ // (see further on in this method). Recording subsequent device selections
+ // after the first would provide inflated durations that don't truly
+ // reflect how long it took for the user to find the device they're
+ // looking for.
+ return;
+ }
+
+ chrome.bluetoothPrivate.recordDeviceSelection(
+ Date.now() - this.discoveryStartTimestampMs_, wasPaired, transport);
+
+ this.discoveryStartTimestampMs_ = null;
+ },
});
diff --git a/device/bluetooth/chromeos/bluetooth_utils.cc b/device/bluetooth/chromeos/bluetooth_utils.cc
index 69194dd4e..7b5a658 100644
--- a/device/bluetooth/chromeos/bluetooth_utils.cc
+++ b/device/bluetooth/chromeos/bluetooth_utils.cc
@@ -8,10 +8,12 @@
#include "base/feature_list.h"
#include "base/metrics/field_trial_params.h"
+#include "base/metrics/histogram_functions.h"
#include "base/optional.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
+#include "base/time/time.h"
#include "chromeos/constants/chromeos_features.h"
#include "device/base/features.h"
@@ -27,6 +29,9 @@
const size_t kLongTermKeyHexStringLength = 32;
+constexpr base::TimeDelta kMaxDeviceSelectionDuration =
+ base::TimeDelta::FromSeconds(30);
+
// Get limited number of devices from |devices| and
// prioritize paired/connecting devices over other devices.
BluetoothAdapter::DeviceList GetLimitedNumDevices(
@@ -97,6 +102,13 @@
return result;
}
+void RecordDeviceSelectionDuration(const std::string& histogram_name,
+ base::TimeDelta duration) {
+ base::UmaHistogramCustomTimes(
+ histogram_name, duration, base::TimeDelta::FromMilliseconds(1) /* min */,
+ kMaxDeviceSelectionDuration /* max */, 50 /* buckets */);
+}
+
} // namespace
device::BluetoothAdapter::DeviceList FilterBluetoothDeviceList(
@@ -134,4 +146,51 @@
return long_term_keys;
}
+void RecordDeviceSelectionDuration(base::TimeDelta duration,
+ BluetoothUiSurface surface,
+ bool was_paired,
+ BluetoothTransport transport) {
+ // Throw out longtail results of the user taking longer than
+ // |kMaxDeviceSelectionDuration|. Assume that these thrown out results reflect
+ // the user not being actively engaged with device connection: leaving the
+ // page open for a long time, walking away from computer, etc.
+ if (duration > kMaxDeviceSelectionDuration)
+ return;
+
+ std::string base_histogram_name =
+ "Bluetooth.ChromeOS.DeviceSelectionDuration";
+ RecordDeviceSelectionDuration(base_histogram_name, duration);
+
+ std::string surface_name =
+ (surface == BluetoothUiSurface::kSettings ? "Settings" : "SystemTray");
+ std::string surface_histogram_name = base_histogram_name + "." + surface_name;
+ RecordDeviceSelectionDuration(surface_histogram_name, duration);
+
+ std::string paired_name = (was_paired ? "Paired" : "NotPaired");
+ std::string paired_histogram_name =
+ surface_histogram_name + "." + paired_name;
+ RecordDeviceSelectionDuration(paired_histogram_name, duration);
+
+ if (!was_paired) {
+ std::string transport_name;
+ switch (transport) {
+ case BLUETOOTH_TRANSPORT_CLASSIC:
+ transport_name = "Classic";
+ break;
+ case BLUETOOTH_TRANSPORT_LE:
+ transport_name = "BLE";
+ break;
+ case BLUETOOTH_TRANSPORT_DUAL:
+ transport_name = "Dual";
+ break;
+ default:
+ return;
+ }
+
+ std::string transport_histogram_name =
+ paired_histogram_name + "." + transport_name;
+ RecordDeviceSelectionDuration(transport_histogram_name, duration);
+ }
+}
+
} // namespace device
diff --git a/device/bluetooth/chromeos/bluetooth_utils.h b/device/bluetooth/chromeos/bluetooth_utils.h
index 32ab5c02..12aa626f 100644
--- a/device/bluetooth/chromeos/bluetooth_utils.h
+++ b/device/bluetooth/chromeos/bluetooth_utils.h
@@ -10,6 +10,10 @@
#include "device/bluetooth/bluetooth_adapter.h"
#include "device/bluetooth/bluetooth_export.h"
+namespace base {
+class TimeDelta;
+} // namespace base
+
// This file contains common utilities, including filtering bluetooth devices
// based on the filter criteria.
namespace device {
@@ -22,6 +26,11 @@
KNOWN,
};
+enum class BluetoothUiSurface {
+ kSettings,
+ kSystemTray,
+};
+
// Return filtered devices based on the filter type and max number of devices.
device::BluetoothAdapter::DeviceList DEVICE_BLUETOOTH_EXPORT
FilterBluetoothDeviceList(const BluetoothAdapter::DeviceList& devices,
@@ -31,6 +40,14 @@
std::vector<std::vector<uint8_t>> DEVICE_BLUETOOTH_EXPORT
GetBlockedLongTermKeys();
+// Record how long it took for a user to find and select the device they wished
+// to connect to.
+void DEVICE_BLUETOOTH_EXPORT
+RecordDeviceSelectionDuration(base::TimeDelta duration,
+ BluetoothUiSurface surface,
+ bool was_paired,
+ BluetoothTransport transport);
+
} // namespace device
#endif // DEVICE_BLUETOOTH_CHROMEOS_BLUETOOTH_UTILS_H_
diff --git a/extensions/browser/api/bluetooth/bluetooth_private_api.cc b/extensions/browser/api/bluetooth/bluetooth_private_api.cc
index 0b9acda..d88298e 100644
--- a/extensions/browser/api/bluetooth/bluetooth_private_api.cc
+++ b/extensions/browser/api/bluetooth/bluetooth_private_api.cc
@@ -26,6 +26,10 @@
#include "extensions/common/api/bluetooth.h"
#include "extensions/common/api/bluetooth_private.h"
+#if defined(OS_CHROMEOS)
+#include "device/bluetooth/chromeos/bluetooth_utils.h"
+#endif
+
namespace bt = extensions::api::bluetooth;
namespace bt_private = extensions::api::bluetooth_private;
namespace SetDiscoveryFilter = bt_private::SetDiscoveryFilter;
@@ -675,6 +679,8 @@
Respond(Error(kPairingFailed));
}
+////////////////////////////////////////////////////////////////////////////////
+
BluetoothPrivateRecordPairingFunction::BluetoothPrivateRecordPairingFunction() =
default;
@@ -693,6 +699,8 @@
RecordPairingTransport(params_->transport);
}
+////////////////////////////////////////////////////////////////////////////////
+
BluetoothPrivateRecordReconnectionFunction::
BluetoothPrivateRecordReconnectionFunction() = default;
@@ -716,6 +724,44 @@
////////////////////////////////////////////////////////////////////////////////
+BluetoothPrivateRecordDeviceSelectionFunction::
+ BluetoothPrivateRecordDeviceSelectionFunction() = default;
+
+BluetoothPrivateRecordDeviceSelectionFunction::
+ ~BluetoothPrivateRecordDeviceSelectionFunction() = default;
+
+bool BluetoothPrivateRecordDeviceSelectionFunction::CreateParams() {
+ params_ = bt_private::RecordDeviceSelection::Params::Create(*args_);
+ return params_ != nullptr;
+}
+
+void BluetoothPrivateRecordDeviceSelectionFunction::DoWork(
+ scoped_refptr<device::BluetoothAdapter> adapter) {
+#if defined(OS_CHROMEOS)
+ device::BluetoothTransport transport;
+ switch (params_->transport) {
+ case bt::Transport::TRANSPORT_CLASSIC:
+ transport = device::BLUETOOTH_TRANSPORT_CLASSIC;
+ break;
+ case bt::Transport::TRANSPORT_LE:
+ transport = device::BLUETOOTH_TRANSPORT_LE;
+ break;
+ case bt::Transport::TRANSPORT_DUAL:
+ transport = device::BLUETOOTH_TRANSPORT_DUAL;
+ break;
+ default:
+ transport = device::BLUETOOTH_TRANSPORT_INVALID;
+ break;
+ }
+
+ device::RecordDeviceSelectionDuration(
+ base::TimeDelta::FromMilliseconds(params_->selection_duration_ms),
+ device::BluetoothUiSurface::kSettings, params_->was_paired, transport);
+#endif
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
} // namespace api
} // namespace extensions
diff --git a/extensions/browser/api/bluetooth/bluetooth_private_api.h b/extensions/browser/api/bluetooth/bluetooth_private_api.h
index 57cfaa1d..ec8b0e5 100644
--- a/extensions/browser/api/bluetooth/bluetooth_private_api.h
+++ b/extensions/browser/api/bluetooth/bluetooth_private_api.h
@@ -76,6 +76,9 @@
namespace RecordReconnection {
struct Params;
} // namespace RecordReconnection
+namespace RecordDeviceSelection {
+struct Params;
+} // namespace RecordDeviceSelection
} // namespace bluetooth_private
class BluetoothPrivateSetAdapterStateFunction
@@ -282,6 +285,27 @@
DISALLOW_COPY_AND_ASSIGN(BluetoothPrivateRecordReconnectionFunction);
};
+class BluetoothPrivateRecordDeviceSelectionFunction
+ : public BluetoothExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("bluetoothPrivate.recordDeviceSelection",
+ BLUETOOTHPRIVATE_RECORDDEVICESELECTION)
+
+ BluetoothPrivateRecordDeviceSelectionFunction();
+
+ protected:
+ ~BluetoothPrivateRecordDeviceSelectionFunction() override;
+
+ // BluetoothExtensionFunction:
+ bool CreateParams() override;
+ void DoWork(scoped_refptr<device::BluetoothAdapter> adapter) override;
+
+ private:
+ std::unique_ptr<bluetooth_private::RecordDeviceSelection::Params> params_;
+
+ DISALLOW_COPY_AND_ASSIGN(BluetoothPrivateRecordDeviceSelectionFunction);
+};
+
} // namespace api
} // namespace extensions
diff --git a/extensions/browser/extension_function_histogram_value.h b/extensions/browser/extension_function_histogram_value.h
index 03f6fd1..bd3b910 100644
--- a/extensions/browser/extension_function_histogram_value.h
+++ b/extensions/browser/extension_function_histogram_value.h
@@ -1405,6 +1405,7 @@
LOGINSCREENUI_CLOSE = 1342,
DECLARATIVENETREQUEST_GETMATCHEDRULES = 1343,
DECLARATIVENETREQUEST_SETACTIONCOUNTASBADGETEXT = 1344,
+ BLUETOOTHPRIVATE_RECORDDEVICESELECTION = 1345,
// Last entry: Add new entries above, then run:
// python tools/metrics/histograms/update_extension_histograms.py
ENUM_BOUNDARY
diff --git a/extensions/common/api/bluetooth_private.idl b/extensions/common/api/bluetooth_private.idl
index 55840a3..62c39d6 100644
--- a/extensions/common/api/bluetooth_private.idl
+++ b/extensions/common/api/bluetooth_private.idl
@@ -159,6 +159,11 @@
// Record that a user-initiated reconnection attempt to an already paired
// device finished. Do not record cancellations.
static void recordReconnection(boolean success);
+
+ // Record that a user selected a device to connect to.
+ static void recordDeviceSelection(long selectionDurationMs,
+ boolean wasPaired,
+ bluetooth.Transport transport);
};
interface Events {
diff --git a/third_party/closure_compiler/externs/bluetooth_private.js b/third_party/closure_compiler/externs/bluetooth_private.js
index 0ba0571..d53aff1 100644
--- a/third_party/closure_compiler/externs/bluetooth_private.js
+++ b/third_party/closure_compiler/externs/bluetooth_private.js
@@ -173,6 +173,14 @@
chrome.bluetoothPrivate.recordReconnection = function(success) {};
/**
+ * Record that a user selected a device to connect to.
+ * @param {number} selectionDurationMs
+ * @param {boolean} wasPaired
+ * @param {!chrome.bluetooth.Transport} transport
+ */
+chrome.bluetoothPrivate.recordDeviceSelection = function(selectionDurationMs, wasPaired, transport) {};
+
+/**
* Fired when a pairing event occurs.
* @type {!ChromeEvent}
*/
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index 5c3d26e..00b5984 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -19717,6 +19717,7 @@
<int value="1342" label="LOGINSCREENUI_CLOSE"/>
<int value="1343" label="DECLARATIVENETREQUEST_GETMATCHEDRULES"/>
<int value="1344" label="DECLARATIVENETREQUEST_SETACTIONCOUNTASBADGETEXT"/>
+ <int value="1345" label="BLUETOOTHPRIVATE_RECORDDEVICESELECTION"/>
</enum>
<enum name="ExtensionIconState">
diff --git a/tools/metrics/histograms/histograms.xml b/tools/metrics/histograms/histograms.xml
index 8e85ebc..c9d97ae 100644
--- a/tools/metrics/histograms/histograms.xml
+++ b/tools/metrics/histograms/histograms.xml
@@ -14331,6 +14331,20 @@
</summary>
</histogram>
+<histogram name="Bluetooth.ChromeOS.DeviceSelectionDuration" units="ms"
+ expires_after="2020-06-13">
+<!-- Name completed by histogram_suffixes name="BluetoothUISurfaces",
+ name="BluetoothPairingStates", and name="BluetoothTransportTypes" -->
+
+ <owner>hansberry@chromium.org</owner>
+ <owner>jlklein@chromium.org</owner>
+ <summary>
+ Records how long it takes for the user to select a device either after they
+ open the UI and Bluetooth is on, or after Bluetooth turns on while the UI is
+ open.
+ </summary>
+</histogram>
+
<histogram name="Bluetooth.ChromeOS.Pairing.Duration.Failure" units="ms"
expires_after="2020-06-06">
<!-- Name completed by histogram_suffixes name="BluetoothTransportTypes" -->
@@ -149051,10 +149065,23 @@
<affected-histogram name="Blink.VisibleLoadTime.LazyLoadImages.BelowTheFold"/>
</histogram_suffixes>
+<histogram_suffixes name="BluetoothPairedStates" separator=".">
+ <suffix name="NotPaired"/>
+ <suffix name="Paired"/>
+ <affected-histogram
+ name="Bluetooth.ChromeOS.DeviceSelectionDuration.Settings"/>
+ <affected-histogram
+ name="Bluetooth.ChromeOS.DeviceSelectionDuration.SystemTray"/>
+</histogram_suffixes>
+
<histogram_suffixes name="BluetoothTransportTypes" separator=".">
<suffix name="BLE"/>
<suffix name="Classic"/>
<suffix name="Dual"/>
+ <affected-histogram
+ name="Bluetooth.ChromeOS.DeviceSelectionDuration.Settings.NotPaired"/>
+ <affected-histogram
+ name="Bluetooth.ChromeOS.DeviceSelectionDuration.SystemTray.NotPaired"/>
<affected-histogram name="Bluetooth.ChromeOS.Pairing.Duration.Failure"/>
<affected-histogram name="Bluetooth.ChromeOS.Pairing.Duration.Success"/>
<affected-histogram name="Bluetooth.ChromeOS.Pairing.Result"/>
@@ -149063,6 +149090,7 @@
<histogram_suffixes name="BluetoothUISurfaces" separator=".">
<suffix name="Settings"/>
<suffix name="SystemTray"/>
+ <affected-histogram name="Bluetooth.ChromeOS.DeviceSelectionDuration"/>
<affected-histogram
name="Bluetooth.ChromeOS.UserInitiatedReconnectionAttempt.Result"/>
</histogram_suffixes>